package net.karneim.pojobuilder.testenv; import com.google.common.base.Throwables; import javax.annotation.processing.Processor; import javax.tools.*; import javax.tools.JavaFileObject.Kind; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.lang.reflect.UndeclaredThrowableException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * The {@link JavaProject} is a driver for controlling a simple java project. This includes adding source files, * enabling annotation processors, compiling and accessing the generated classes. * * @author Michael Karneim */ public class JavaProject { public enum Compilation {NotStarted, Success, Failure} private Compilation status = Compilation.NotStarted; private final File workingDirectory; private final File outputRoot; private final File localTempDir; private final List<File> sourceFiles = new ArrayList<File>(); private final List<String> classnamesForProcessing = new ArrayList<String>(); private final List<Class<? extends Processor>> processorClasses = new ArrayList<Class<? extends Processor>>(); private final List<Processor> processors = new ArrayList<Processor>(); private final JavaCompiler compiler; private final DiagnosticCollector<JavaFileObject> diagnostics; private final StandardJavaFileManager fileManager; /** * Creates a new java project with the specified working directory. * * @param workingDirectory will be used to store generated source and class files */ public JavaProject(File workingDirectory) { this.workingDirectory = workingDirectory; this.outputRoot = new File(workingDirectory, "output"); this.outputRoot.mkdir(); this.localTempDir = new File(workingDirectory, "temp"); this.localTempDir.mkdir(); this.compiler = ToolProvider.getSystemJavaCompiler(); this.diagnostics = new DiagnosticCollector<JavaFileObject>(); this.fileManager = compiler.getStandardFileManager(diagnostics, null, null); try { // Define the output locations fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(outputRoot)); fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, Arrays.asList(outputRoot)); } catch (IOException e) { throw new UndeclaredThrowableException(e); } } /** * Deletes this java project by deleting the working directory. */ public void delete() { status = Compilation.NotStarted; Util.deleteDir(workingDirectory); } /** * Returns the annotation processors that will be used during the compilation task. * * @return the annotation processors that will be used during the compilation task */ public List<Processor> getProcessors() { return processors; } /** * Returns the annotation processor classes that will be used during the compilation task. * * @return the annotation processor classes that will be used */ public List<Class<? extends Processor>> getProcessorClasses() { return processorClasses; } /** * Returns the directory that contains the generated source and class files. * * @return the directory that contains the generated source and class files */ public File getOutputRoot() { return outputRoot; } public List<Diagnostic<? extends JavaFileObject>> getDiagnostics() { return diagnostics.getDiagnostics(); } /** * Adds the file with the given relative filename to the source tree. If the file is a directory then all files inside * that directory are added (recursively). * * @param filepath the filepath must be absolute or relative to the current directory (that is the directory this JVM * has been started from as stored in System.getProperty("user.dir")) */ public void addSourceFile(String filepath) { File file = new File(filepath); if (file.isAbsolute()) { addSourceFile(file); } else { String curDir = System.getProperty("user.dir"); File absfile = new File(curDir, filepath); addSourceFile(absfile); } } /** * Adds a source file for the given qualified class name and the given content to the source tree. * * @param qualifiedClassname the qualified name of the Java class * @param content the source code of the Java class * @throws IOException */ public void addSourceFile(String qualifiedClassname, String content) throws IOException { File file = new File(localTempDir, getSourceFilename(qualifiedClassname)); file.getParentFile().mkdirs(); PrintWriter out = new PrintWriter(file); try { out.print(content); } finally { out.close(); } addSourceFile(file); } protected static String getSourceFilename(String fullQualifiedClassname) { String result = fullQualifiedClassname.replace('.', '/').concat(".java"); return result; } private void addSourceFile(File aFile) { if (aFile.exists() == false) { return; } if (aFile.isDirectory()) { File[] files = aFile.listFiles(); for (File file : files) { addSourceFile(file); } } else if (aFile.getName().endsWith(".java")) { sourceFiles.add(aFile); } } /** * Adds the (compiled) class with the given full qualified name to the list of classes, that should be processed by * the annotation processor(s) without being compiled first. Make sure that the class is available in the current * class path. * * @param name the full qualified class name of the class */ public void addClassnameForProcessing(String name) { classnamesForProcessing.add(name); } /** * Loads a class with the given class name from this project's output directory and returns it. * * @param classname * @return the class with the given class name, loaded from this project's output directory * @throws ClassNotFoundException */ public Class<?> findClass(String classname) throws ClassNotFoundException { ClassLoader cl = fileManager.getClassLoader(StandardLocation.CLASS_OUTPUT); Class<?> result = cl.loadClass(classname); return result; } /** * Returns an {@link InputStream} for reading the source code of the specified java class with the given name from * this project's output directory. * * @param classname * @return the input stream for reading the specified java class * @throws IOException */ public InputStream findGeneratedSource(String classname) throws IOException { JavaFileObject fileObject = fileManager.getJavaFileForInput(StandardLocation.CLASS_OUTPUT, classname, Kind.SOURCE); if (fileObject == null) { return null; } return fileObject.openInputStream(); } /** * Compiles this project's sources. All generated files will be placed into the output directory. * * @return <code>true</code> if the compilation has been successful. * @throws IOException */ public boolean compile() { try { return _compile(); } catch( Exception e) { throw Throwables.propagate(e); } } private boolean _compile() throws Exception { Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(this.sourceFiles); List<String> optionList = new ArrayList<String>(); // set compiler's class path to be same as the runtime's // optionList.addAll(Arrays.asList("-classpath",System.getProperty("java.class.path"))); // enable the annotation processor if (!processorClasses.isEmpty()) { StringBuilder buf = new StringBuilder(); for (Class<? extends Processor> cls : processorClasses) { if (buf.length() > 0) { buf.append(","); } buf.append(cls.getCanonicalName()); } optionList.addAll(Arrays.asList("-processor", buf.toString())); } optionList.addAll(Arrays.asList("-encoding", "utf8")); JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, optionList, classnamesForProcessing, compilationUnits); if (!processors.isEmpty()) { task.setProcessors(processors); } status = task.call() ? Compilation.Success : Compilation.Failure; fileManager.close(); for (Diagnostic<? extends JavaFileObject> d : diagnostics.getDiagnostics()) { System.out.println(d); } return status == Compilation.Success; } public Compilation getStatus() { return status; } }