package org.jetbrains.jps.incremental.java;
import com.intellij.ant.PseudoClassLoader;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.uiDesigner.compiler.AlienFormFileException;
import com.intellij.uiDesigner.compiler.Utils;
import com.intellij.uiDesigner.lw.CompiledClassPropertiesProvider;
import com.intellij.uiDesigner.lw.LwRootContainer;
import org.jetbrains.jps.Module;
import org.jetbrains.jps.ModuleChunk;
import org.jetbrains.jps.ProjectPaths;
import org.jetbrains.jps.incremental.Builder;
import org.jetbrains.jps.incremental.CompileContext;
import org.jetbrains.jps.incremental.FileProcessor;
import org.jetbrains.jps.incremental.ProjectBuildException;
import org.jetbrains.jps.incremental.messages.BuildMessage;
import org.jetbrains.jps.incremental.messages.CompilerMessage;
import org.jetbrains.jps.incremental.messages.ProgressMessage;
import org.jetbrains.jps.incremental.storage.TimestampStorage;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.EmptyVisitor;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ExecutorService;
/**
* @author Eugene Zhuravlev
* Date: 9/21/11
*/
public class JavaBuilder extends Builder{
public static final String BUILDER_NAME = "java";
public static final String JAVA_EXTENSION = ".java";
public static final String FORM_EXTENSION = ".form";
private static final Key<PseudoClassLoader> PSEUDO_CLASSLOADER_KEY = Key.create("_preudo_class_loader");
private static final FileFilter JAVA_SOURCES_FILTER = new FileFilter() {
public boolean accept(File file) {
return file.getPath().endsWith(JAVA_EXTENSION);
}
};
private static final FileFilter FORM_SOURCES_FILTER = new FileFilter() {
public boolean accept(File file) {
return file.getPath().endsWith(FORM_EXTENSION);
}
};
private static final String JAVAC_COMPILER_NAME = "javac";
private final EmbeddedJavac myJavacCompiler;
public JavaBuilder(ExecutorService tasksExecutor) {
myJavacCompiler = new EmbeddedJavac(tasksExecutor);
//add here class processors in the sequence they should be executed
myJavacCompiler.addClassProcessor(new EmbeddedJavac.ClassPostProcessor() {
public void process(CompileContext context, OutputFileObject out) {
final PseudoClassLoader loader = PSEUDO_CLASSLOADER_KEY.get(context);
if (loader != null) {
final String className = out.getClassName();
if (className != null) {
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (loader) {
loader.defineClass(className.replace('.', '/'), out.getContent().toByteArray());
}
}
}
}
});
}
public String getDescription() {
return "Java Builder";
}
public ExitCode build(final CompileContext context, final ModuleChunk chunk) throws ProjectBuildException {
try {
final TimestampStorage tsStorage = context.getBuildDataManager().getTimestampStorage(BUILDER_NAME);
final Set<File> filesToCompile = new HashSet<File>();
final List<File> formsToCompile = new ArrayList<File>();
final Set<String> srcRoots = new HashSet<String>();
context.processFiles(chunk, new FileProcessor() {
public boolean apply(Module module, File file, String sourceRoot) throws Exception {
if (JAVA_SOURCES_FILTER.accept(file)) {
srcRoots.add(sourceRoot);
if (isFileDirty(file, context, tsStorage)) {
filesToCompile.add(file);
}
}
else if (FORM_SOURCES_FILTER.accept(file)){
if (isFileDirty(file, context, tsStorage)) {
formsToCompile.add(file);
}
}
return true;
}
});
for (File form : formsToCompile) {
for (String root : srcRoots) {
final File boundSource = getBoundSource(root, form);
if (boundSource != null) {
// force compilation of classes that modified forms are bound to
filesToCompile.add(boundSource);
break;
}
}
}
return compile(context, chunk, filesToCompile, formsToCompile);
}
catch (Exception e) {
String message = e.getMessage();
if (message == null) {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
e.printStackTrace(new PrintStream(out));
message = "Internal error: \n" + out.toString();
}
throw new ProjectBuildException(message, e);
}
}
private File getBoundSource(String srcRoot, File formFile) {
final String boundClassName = getBoundClassName(formFile);
if (boundClassName == null) {
return null;
}
String relPath = boundClassName.replace('.', '/') + JAVA_EXTENSION;
while (true) {
final File candidate = new File(srcRoot, relPath);
if (candidate.exists()) {
return candidate.isFile()? candidate : null;
}
final int index = relPath.lastIndexOf('/');
if (index <= 0) {
return null;
}
relPath = relPath.substring(0, index) + JAVA_EXTENSION;
}
}
private ExitCode compile(final CompileContext context, ModuleChunk chunk, Collection<File> files, Collection<File> forms) throws Exception {
if (files.isEmpty() && forms.isEmpty()) {
return ExitCode.OK;
}
ProjectPaths paths = ProjectPaths.KEY.get(context);
if (paths == null) {
ProjectPaths.KEY.set(context, paths = new ProjectPaths(context.getProject()));
}
final Collection<File> classpath = paths.getCompilationClasspath(chunk, context.isCompilingTests(), !context.isMake());
final Collection<File> platformCp = paths.getPlatformCompilationClasspath(chunk, context.isCompilingTests(), !context.isMake());
final Map<File, Set<File>> outs = buildOutputDirectoriesMap(context, chunk);
final List<String> options = getCompilationOptions(context, chunk);
final TimestampStorage tsStorage = context.getBuildDataManager().getTimestampStorage(BUILDER_NAME);
// setup loader for instrumentation
final List<URL> urls = new ArrayList<URL>();
for (Collection<File> cp : Arrays.asList(platformCp, classpath)) {
for (File file : cp) {
urls.add(file.toURI().toURL());
}
}
final PseudoClassLoader pseudoLoader = new PseudoClassLoader(urls.toArray(new URL[urls.size()]));
PSEUDO_CLASSLOADER_KEY.set(context, pseudoLoader);
final DiagnosticSink diagnosticSink = new DiagnosticSink(context);
final OutputFilesSink outputSink = new OutputFilesSink(context);
try {
final boolean compilationOk = myJavacCompiler.compile(options, files, classpath, platformCp, outs, context, diagnosticSink, outputSink);
if (!compilationOk || diagnosticSink.getErrorCount() > 0) {
throw new ProjectBuildException("Compilation failed: errors: " + diagnosticSink.getErrorCount() + "; warnings: " + diagnosticSink.getWarningCount());
}
instrumentForms(context, pseudoLoader, forms, outs);
// todo: add notNull
return ExitCode.OK;
}
finally {
outputSink.writePendingData();
PSEUDO_CLASSLOADER_KEY.set(context, null);
for (File file : outputSink.getSuccessfullyCompiled()) {
tsStorage.saveStamp(file);
}
}
}
private static List<String> getCompilationOptions(CompileContext context, ModuleChunk chunk) {
return Arrays.asList("-verbose")/*Collections.emptyList()*/;
}
private static Map<File, Set<File>> buildOutputDirectoriesMap(CompileContext context, ModuleChunk chunk) {
final Map<File, Set<File>> map = new HashMap<File, Set<File>>();
final boolean compilingTests = context.isCompilingTests();
for (Module module : chunk.getModules()) {
final String outputPath;
final Collection<String> srcPaths;
if (compilingTests) {
outputPath = module.getTestOutputPath();
srcPaths = (Collection<String>)module.getTestRoots();
}
else {
outputPath = module.getOutputPath();
srcPaths = (Collection<String>)module.getSourceRoots();
}
final Set<File> roots = new HashSet<File>();
for (String path : srcPaths) {
roots.add(new File(path));
}
map.put(new File(outputPath), roots);
}
return map;
}
private String getBoundClassName(File formFile) {
return null; // todo
}
private void instrumentForms(CompileContext context, final PseudoClassLoader loader, Collection<File> formsToInstrument, Map<File, Set<File>> outs) throws ProjectBuildException {
final Map<String, File> class2form = new HashMap<String, File>();
for (File formFile : formsToInstrument) {
final File outputDir = findOutputDir(formFile, outs);
if (outputDir == null) {
context.processMessage(new CompilerMessage(JavaBuilder.JAVAC_COMPILER_NAME, BuildMessage.Kind.ERROR, "No output directory found for the form", formFile.getAbsolutePath()));
continue;
}
final LwRootContainer rootContainer;
try {
rootContainer = Utils.getRootContainer(formFile.toURI().toURL(), new CompiledClassPropertiesProvider(loader.getLoader()));
}
catch (AlienFormFileException e) {
// ignore non-IDEA forms
continue;
}
catch (Exception e) {
throw new ProjectBuildException("Cannot process form file " + formFile.getAbsolutePath(), e);
}
final String classToBind = rootContainer.getClassToBind();
if (classToBind == null) {
continue;
}
// todo: temporarily commented
//final File classFile = getClassFile(outputDir, classToBind.replace('.', '/'));
//if (classFile == null) {
// context.processMessage(new CompilerMessage(JavaBuilder.JAVAC_COMPILER_NAME, BuildMessage.Kind.WARNING, "Class to bind does not exist: " + classToBind, formFile.getAbsolutePath()));
// continue;
//}
//
//final File alreadyProcessedForm = class2form.get(classToBind);
//if (alreadyProcessedForm != null) {
// context.processMessage(new CompilerMessage(
// JavaBuilder.JAVAC_COMPILER_NAME, BuildMessage.Kind.WARNING,
// formFile.getAbsolutePath() + ": The form is bound to the class " + classToBind + ".\nAnother form " + alreadyProcessedForm.getAbsolutePath() + " is also bound to this class",
// formFile.getAbsolutePath())
// );
// continue;
//}
//
//class2form.put(classToBind, formFile);
//
//try {
// int version;
// InputStream stream = new FileInputStream(classFile);
// try {
// version = getClassFileVersion(new ClassReader(stream));
// }
// finally {
// stream.close();
// }
// AntClassWriter classWriter = new AntClassWriter(getAsmClassWriterFlags(version), loader);
// AntNestedFormLoader formLoader = new AntNestedFormLoader(loader.getLoader(), myNestedFormPathList);
// final AsmCodeGenerator codeGenerator = new AsmCodeGenerator(rootContainer, loader.getLoader(), formLoader, false, classWriter);
// codeGenerator.patchFile(classFile);
//
// final FormErrorInfo[] warnings = codeGenerator.getWarnings();
// for (final FormErrorInfo warning : warnings) {
// context.processMessage(new CompilerMessage(JAVAC_COMPILER_NAME, BuildMessage.Kind.WARNING, warning.getErrorMessage(), formFile.getAbsolutePath()));
// }
//
// final FormErrorInfo[] errors = codeGenerator.getErrors();
// if (errors.length > 0) {
// StringBuilder message = new StringBuilder();
// for (final FormErrorInfo error : errors) {
// if (message.length() > 0) {
// message.append("\n");
// }
// message.append(formFile.getAbsolutePath()).append(": ").append(error.getErrorMessage());
// }
// context.processMessage(new CompilerMessage(JAVAC_COMPILER_NAME, BuildMessage.Kind.ERROR, message.toString()));
// }
//}
//catch (Exception e) {
// context.processMessage(new CompilerMessage(JAVAC_COMPILER_NAME, BuildMessage.Kind.ERROR, "Forms instrumentation failed" + e.getMessage(), formFile.getAbsolutePath()));
//}
}
}
private static File findOutputDir(File src, Map<File, Set<File>> outs) {
if (outs.isEmpty()) {
return null;
}
if (outs.size() == 1) {
return outs.keySet().iterator().next();
}
File file = src.getParentFile();
while (file != null) {
for (Map.Entry<File, Set<File>> entry : outs.entrySet()) {
if (entry.getValue().contains(file)) {
return entry.getKey();
}
}
file = file.getParentFile();
}
return null;
}
//private File getClassFile(File outputDir, String className) {
// final String classOrInnerName = getClassOrInnerName(outputDir, className);
// return classOrInnerName != null? new File(outputDir, classOrInnerName + ".class") : null;
//}
//private String getClassOrInnerName(final File outputDir, String className) {
// final File classFile = new File(outputDir, className + ".class");
// if (classFile.exists()) {
// return className;
// }
// int position = className.lastIndexOf('/');
// if (position == -1) {
// return null;
// }
// return getClassOrInnerName(outputDir, className.substring(0, position) + '$' + className.substring(position + 1));
//}
private static int getClassFileVersion(ClassReader reader) {
final Ref<Integer> result = new Ref<Integer>(0);
reader.accept(new EmptyVisitor() {
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
result.set(version);
}
}, 0);
return result.get();
}
private static int getAsmClassWriterFlags(int version) {
return version >= Opcodes.V1_6 && version != Opcodes.V1_1 ? ClassWriter.COMPUTE_FRAMES : ClassWriter.COMPUTE_MAXS;
}
private static class DiagnosticSink implements EmbeddedJavac.DiagnosticOutputConsumer {
private final CompileContext myContext;
private volatile int myErrorCount = 0;
private volatile int myWarningCount = 0;
public DiagnosticSink(CompileContext context) {
myContext = context;
}
public void outputLineAvailable(String line) {
myContext.processMessage(new CompilerMessage(JAVAC_COMPILER_NAME, BuildMessage.Kind.INFO, line));
}
public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
final CompilerMessage.Kind kind;
switch (diagnostic.getKind()) {
case ERROR:
kind = BuildMessage.Kind.ERROR;
myErrorCount++;
break;
case MANDATORY_WARNING:
case WARNING:
kind = BuildMessage.Kind.WARNING;
myWarningCount++;
break;
default:
kind = BuildMessage.Kind.INFO;
}
final String srcPath;
final JavaFileObject source = diagnostic.getSource();
if (source != null) {
srcPath = FileUtil.toSystemIndependentName(new File(source.toUri()).getPath());
}
else {
srcPath = null;
}
myContext.processMessage(new CompilerMessage(
JAVAC_COMPILER_NAME, kind, diagnostic.getMessage(Locale.US), srcPath,
diagnostic.getStartPosition(), diagnostic.getEndPosition(), diagnostic.getPosition(),
diagnostic.getLineNumber(), diagnostic.getColumnNumber()
));
}
public int getErrorCount() {
return myErrorCount;
}
public int getWarningCount() {
return myWarningCount;
}
}
private static class OutputFilesSink implements EmbeddedJavac.OutputFileConsumer {
private final CompileContext myContext;
private final Set<File> mySuccessfullyCompiled = new HashSet<File>();
private final List<OutputFileObject> myUnsavedFiles = new ArrayList<OutputFileObject>();
public OutputFilesSink(CompileContext context) {
myContext = context;
}
public void save(OutputFileObject fileObject) {
try {
if (shouldInstrument(fileObject)) {
myUnsavedFiles.add(fileObject);
}
else {
writeToDisk(fileObject);
}
}
catch (IOException e) {
myContext.processMessage(new CompilerMessage(JAVAC_COMPILER_NAME, BuildMessage.Kind.ERROR, e.getMessage()));
}
}
public List<OutputFileObject> getUnsavedFiles() {
return myUnsavedFiles;
}
public void writePendingData() {
for (OutputFileObject file : myUnsavedFiles) {
try {
writeToDisk(file);
}
catch (IOException e) {
myContext.processMessage(new CompilerMessage(JAVAC_COMPILER_NAME, BuildMessage.Kind.ERROR, e.getMessage()));
}
}
}
public Set<File> getSuccessfullyCompiled() {
return mySuccessfullyCompiled;
}
private boolean shouldInstrument(OutputFileObject fileObject) {
return false; // todo!!!
}
private void writeToDisk(OutputFileObject fileObject) throws IOException {
final OutputFileObject.Content content = fileObject.getContent();
if (content != null) {
FileUtil.writeToFile(fileObject.getFile(), content.getBuffer(), content.getOffset(), content.getLength());
}
else {
throw new IOException("Missing content for file " + fileObject.getFile());
}
myContext.processMessage(new ProgressMessage("Compiled " + fileObject.getFile().getPath()));
final JavaFileObject source = fileObject.getSource();
if (source != null) {
final File file = new File(source.toUri());
synchronized (mySuccessfullyCompiled) {
mySuccessfullyCompiled.add(file);
}
}
}
}
// todo: temporarily commented
//private class AntNestedFormLoader implements NestedFormLoader {
// private final ClassLoader myLoader;
// private final List myNestedFormPathList;
// private final HashMap myFormCache = new HashMap();
//
// public AntNestedFormLoader(final ClassLoader loader, List nestedFormPathList) {
// myLoader = loader;
// myNestedFormPathList = nestedFormPathList;
// }
//
// public LwRootContainer loadForm(String formFilePath) throws Exception {
// if (myFormCache.containsKey(formFilePath)) {
// return (LwRootContainer)myFormCache.get(formFilePath);
// }
//
// String lowerFormFilePath = formFilePath.toLowerCase();
// for (Iterator iterator = myFormFiles.iterator(); iterator.hasNext();) {
// File file = (File)iterator.next();
// String name = file.getAbsolutePath().replace(File.separatorChar, '/').toLowerCase();
// if (name.endsWith(lowerFormFilePath)) {
// return loadForm(formFilePath, new FileInputStream(file));
// }
// }
//
// if (myNestedFormPathList != null) {
// for (int i = 0; i < myNestedFormPathList.size(); i++) {
// PrefixedPath path = (PrefixedPath)myNestedFormPathList.get(i);
// File formFile = path.findFile(formFilePath);
// if (formFile != null) {
// return loadForm(formFilePath, new FileInputStream(formFile));
// }
// }
// }
// InputStream resourceStream = myLoader.getResourceAsStream(formFilePath);
// if (resourceStream != null) {
// return loadForm(formFilePath, resourceStream);
// }
// throw new Exception("Cannot find nested form file " + formFilePath);
// }
//
// private LwRootContainer loadForm(String formFileName, InputStream resourceStream) throws Exception {
// final LwRootContainer container = Utils.getRootContainer(resourceStream, null);
// myFormCache.put(formFileName, container);
// return container;
// }
//
// public String getClassToBindName(LwRootContainer container) {
// final String className = container.getClassToBind();
// String result = getClassOrInnerName(className.replace('.', '/'));
// if (result != null) return result.replace('/', '.');
// return className;
// }
//}
}