bd74b495743ff7d93f160564777382dbb833cea0
[idea/community.git] / jps / jps-builders / src / org / jetbrains / jps / incremental / java / JavaBuilder.java
1 package org.jetbrains.jps.incremental.java;
2
3 import com.intellij.compiler.notNullVerification.NotNullVerifyingInstrumenter;
4 import com.intellij.execution.process.BaseOSProcessHandler;
5 import com.intellij.openapi.application.PathManager;
6 import com.intellij.openapi.util.Key;
7 import com.intellij.openapi.util.Ref;
8 import com.intellij.openapi.util.io.FileUtil;
9 import com.intellij.openapi.util.text.StringUtil;
10 import com.intellij.uiDesigner.compiler.*;
11 import com.intellij.uiDesigner.core.GridConstraints;
12 import com.intellij.uiDesigner.lw.CompiledClassPropertiesProvider;
13 import com.intellij.uiDesigner.lw.LwRootContainer;
14 import org.jetbrains.annotations.NotNull;
15 import org.jetbrains.annotations.Nullable;
16 import org.jetbrains.ether.dependencyView.Callbacks;
17 import org.jetbrains.ether.dependencyView.Mappings;
18 import org.jetbrains.jps.Module;
19 import org.jetbrains.jps.ModuleChunk;
20 import org.jetbrains.jps.Project;
21 import org.jetbrains.jps.ProjectPaths;
22 import org.jetbrains.jps.api.GlobalOptions;
23 import org.jetbrains.jps.api.RequestFuture;
24 import org.jetbrains.jps.incremental.*;
25 import org.jetbrains.jps.incremental.messages.BuildMessage;
26 import org.jetbrains.jps.incremental.messages.CompilerMessage;
27 import org.jetbrains.jps.incremental.messages.FileGeneratedEvent;
28 import org.jetbrains.jps.incremental.messages.ProgressMessage;
29 import org.jetbrains.jps.incremental.storage.BuildDataManager;
30 import org.jetbrains.jps.incremental.storage.SourceToFormMapping;
31 import org.jetbrains.jps.javac.*;
32 import org.objectweb.asm.ClassReader;
33 import org.objectweb.asm.ClassWriter;
34 import org.objectweb.asm.Opcodes;
35 import org.objectweb.asm.commons.EmptyVisitor;
36
37 import javax.tools.*;
38 import java.io.*;
39 import java.net.MalformedURLException;
40 import java.net.ServerSocket;
41 import java.net.URL;
42 import java.net.URLClassLoader;
43 import java.util.*;
44 import java.util.concurrent.ExecutionException;
45 import java.util.concurrent.ExecutorService;
46
47 /**
48  * @author Eugene Zhuravlev
49  *         Date: 9/21/11
50  */
51 public class JavaBuilder extends ModuleLevelBuilder {
52   public static final String BUILDER_NAME = "java";
53   private static final String JAVA_EXTENSION = ".java";
54   private static final String FORM_EXTENSION = ".form";
55   public static final boolean USE_EMBEDDED_JAVAC = System.getProperty(GlobalOptions.USE_EXTERNAL_JAVAC_OPTION) == null;
56
57   private static final FileFilter JAVA_SOURCES_FILTER = new FileFilter() {
58     public boolean accept(File file) {
59       return file.getPath().endsWith(JAVA_EXTENSION);
60     }
61   };
62   private static final FileFilter FORM_SOURCES_FILTER = new FileFilter() {
63     public boolean accept(File file) {
64       return file.getPath().endsWith(FORM_EXTENSION);
65     }
66   };
67
68   private static final Key<Callbacks.Backend> DELTA_MAPPINGS_CALLBACK_KEY = Key.create("_dependency_data_");
69   private final ExecutorService myTaskRunner;
70   private final List<ClassPostProcessor> myClassProcessors = new ArrayList<ClassPostProcessor>();
71
72   public JavaBuilder(ExecutorService tasksExecutor) {
73     myTaskRunner = tasksExecutor;
74     //add here class processors in the sequence they should be executed
75     myClassProcessors.add(new ClassPostProcessor() {
76       public void process(CompileContext context, OutputFileObject out) {
77         final Callbacks.Backend callback = DELTA_MAPPINGS_CALLBACK_KEY.get(context);
78         if (callback != null) {
79           final OutputFileObject.Content content = out.getContent();
80           final File srcFile = out.getSourceFile();
81           if (srcFile != null && content != null) {
82             final String outputPath = FileUtil.toSystemIndependentName(out.getFile().getPath());
83             final String sourcePath = FileUtil.toSystemIndependentName(srcFile.getPath());
84             final RootDescriptor moduleAndRoot = context.getModuleAndRoot(srcFile);
85             final BuildDataManager dataManager = context.getDataManager();
86             if (moduleAndRoot != null) {
87               try {
88                 final String moduleName = moduleAndRoot.module.getName().toLowerCase(Locale.US);
89                 dataManager.getSourceToOutputMap(moduleName, context.isCompilingTests()).appendData(sourcePath, outputPath);
90               }
91               catch (Exception e) {
92                 context.processMessage(new CompilerMessage(BUILDER_NAME, e));
93               }
94             }
95             final ClassReader reader = new ClassReader(content.getBuffer(), content.getOffset(), content.getLength());
96             //noinspection SynchronizationOnLocalVariableOrMethodParameter
97             synchronized (dataManager.getMappings()) {
98               callback.associate(outputPath, Callbacks.getDefaultLookup(sourcePath), reader);
99             }
100           }
101         }
102       }
103     });
104   }
105
106   @Override
107   public String getName() {
108     return BUILDER_NAME;
109   }
110
111   public String getDescription() {
112     return "Java Builder";
113   }
114
115   private static final Key<Set<File>> TEMPORARY_SOURCE_ROOTS_KEY = Key.create("_additional_source_roots_");
116
117   public static void addTempSourcePathRoot(CompileContext context, File root) {
118     Set<File> roots = TEMPORARY_SOURCE_ROOTS_KEY.get(context);
119     if (roots == null) {
120       roots = new HashSet<File>();
121       TEMPORARY_SOURCE_ROOTS_KEY.set(context, roots);
122     }
123     roots.add(root);
124   }
125
126   public ExitCode build(final CompileContext context, final ModuleChunk chunk) throws ProjectBuildException {
127     try {
128       final Set<File> filesToCompile = new HashSet<File>();
129       final Set<File> formsToCompile = new HashSet<File>();
130
131       context.processFilesToRecompile(chunk, new FileProcessor() {
132         public boolean apply(Module module, File file, String sourceRoot) throws Exception {
133           if (JAVA_SOURCES_FILTER.accept(file)) {
134             filesToCompile.add(file);
135           }
136           else if (FORM_SOURCES_FILTER.accept(file)) {
137             formsToCompile.add(file);
138           }
139           return true;
140         }
141       });
142
143       // force compilation of bound source file if the form is dirty
144       if (!context.isProjectRebuild()) {
145         for (File form : formsToCompile) {
146           final RootDescriptor descriptor = context.getModuleAndRoot(form);
147           if (descriptor != null) {
148             for (RootDescriptor rd : context.getModuleRoots(descriptor.module)) {
149               final File boundSource = getBoundSource(rd.root, form);
150               if (boundSource != null) {
151                 filesToCompile.add(boundSource);
152                 break;
153               }
154             }
155           }
156         }
157
158         // form should be considered dirty if the class it is bound to is dirty
159         final SourceToFormMapping sourceToFormMap = context.getDataManager().getSourceToFormMap();
160         for (File srcFile : filesToCompile) {
161           final String srcPath = srcFile.getPath();
162           final String formPath = sourceToFormMap.getState(srcPath);
163           if (formPath != null) {
164             final File formFile = new File(formPath);
165             if (formFile.exists()) {
166               context.markDirty(formFile);
167               formsToCompile.add(formFile);
168             }
169             sourceToFormMap.remove(srcPath);
170           }
171         }
172       }
173
174
175       return compile(context, chunk, filesToCompile, formsToCompile);
176     }
177     catch (ProjectBuildException e) {
178       throw e;
179     }
180     catch (Exception e) {
181       String message = e.getMessage();
182       if (message == null) {
183         final ByteArrayOutputStream out = new ByteArrayOutputStream();
184         e.printStackTrace(new PrintStream(out));
185         message = "Internal error: \n" + out.toString();
186       }
187       context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, message));
188       throw new ProjectBuildException(message, e);
189     }
190   }
191
192   @Nullable
193   private static File getBoundSource(File srcRoot, File formFile) throws IOException {
194     final String boundClassName = FormsParsing.readBoundClassName(formFile);
195     if (boundClassName == null) {
196       return null;
197     }
198     String relPath = boundClassName.replace('.', '/') + JAVA_EXTENSION;
199     while (true) {
200       final File candidate = new File(srcRoot, relPath);
201       if (candidate.exists()) {
202         return candidate.isFile()? candidate : null;
203       }
204       final int index = relPath.lastIndexOf('/');
205       if (index <= 0) {
206         return null;
207       }
208       relPath = relPath.substring(0, index) + JAVA_EXTENSION;
209     }
210   }
211
212   private ExitCode compile(final CompileContext context, ModuleChunk chunk, Collection<File> files, Collection<File> forms) throws Exception {
213     ExitCode exitCode = ExitCode.OK;
214
215     final boolean hasSourcesToCompile = !files.isEmpty() || !forms.isEmpty();
216
217     if (!hasSourcesToCompile && !context.hasRemovedSources()) {
218       return exitCode;
219     }
220
221     final ProjectPaths paths = context.getProjectPaths();
222
223     final boolean addNotNullAssertions = context.getProject().getCompilerConfiguration().isAddNotNullAssertions();
224
225     final Collection<File> classpath = paths.getCompilationClasspath(chunk, context.isCompilingTests(), false/*context.isProjectRebuild()*/);
226     final Collection<File> platformCp = paths.getPlatformCompilationClasspath(chunk, context.isCompilingTests(), false/*context.isProjectRebuild()*/);
227     final Map<File, Set<File>> outs = buildOutputDirectoriesMap(context, chunk);
228
229     // begin compilation round
230     final DiagnosticSink diagnosticSink = new DiagnosticSink(context);
231     final OutputFilesSink outputSink = new OutputFilesSink(context);
232     final Mappings delta = context.createDelta();
233     DELTA_MAPPINGS_CALLBACK_KEY.set(context, delta.getCallback());
234     try {
235       if (hasSourcesToCompile) {
236         final Set<File> sourcePath = TEMPORARY_SOURCE_ROOTS_KEY.get(context,Collections.<File>emptySet());
237
238         final boolean compiledOk = compileJava(
239           files, classpath, platformCp, sourcePath, outs, context, diagnosticSink, outputSink
240         );
241
242         final Map<File, String> chunkSourcePath = ProjectPaths.getSourceRootsWithDependents(chunk, context.isCompilingTests());
243         final ClassLoader compiledClassesLoader = createInstrumentationClassLoader(classpath, platformCp, chunkSourcePath, outputSink);
244
245         if (!forms.isEmpty()) {
246           try {
247             context.processMessage(new ProgressMessage("Instrumenting forms [" + chunk.getName() + "]"));
248             instrumentForms(context, chunk, chunkSourcePath, compiledClassesLoader, forms, outputSink);
249           }
250           finally {
251             context.processMessage(new ProgressMessage("Finished instrumenting forms [" + chunk.getName() + "]"));
252           }
253         }
254
255         if (addNotNullAssertions) {
256           try {
257             context.processMessage(new ProgressMessage("Adding NotNull assertions [" + chunk.getName() + "]"));
258             instrumentNotNull(context, outputSink, compiledClassesLoader);
259           }
260           finally {
261             context.processMessage(new ProgressMessage("Finished adding NotNull assertions [" + chunk.getName() + "]"));
262           }
263         }
264
265         if (!compiledOk && diagnosticSink.getErrorCount() == 0) {
266           throw new ProjectBuildException("Compilation failed: internal java compiler error");
267         }
268         if (diagnosticSink.getErrorCount() > 0) {
269           throw new ProjectBuildException("Compilation failed: errors: " + diagnosticSink.getErrorCount() + "; warnings: " + diagnosticSink.getWarningCount());
270         }
271       }
272     }
273     finally {
274       outputSink.writePendingData();
275
276       final Set<File> successfullyCompiled = outputSink.getSuccessfullyCompiled();
277       DELTA_MAPPINGS_CALLBACK_KEY.set(context, null);
278
279       if (updateMappings(context, delta, chunk, files, successfullyCompiled)) {
280         exitCode = ExitCode.ADDITIONAL_PASS_REQUIRED;
281       }
282     }
283
284     if (exitCode != ExitCode.ADDITIONAL_PASS_REQUIRED) {
285       final Set<File> tempRoots = TEMPORARY_SOURCE_ROOTS_KEY.get(context);
286       TEMPORARY_SOURCE_ROOTS_KEY.set(context, null);
287       if (tempRoots != null) {
288         for (File root : tempRoots) {
289           FileUtil.delete(root);
290         }
291       }
292     }
293     return exitCode;
294   }
295
296   private boolean compileJava(Collection<File> files, Collection<File> classpath, Collection<File> platformCp, Collection<File> sourcePath, Map<File, Set<File>> outs, CompileContext context, DiagnosticOutputConsumer diagnosticSink, final OutputFileConsumer outputSink) throws Exception {
297     final List<String> options = getCompilationOptions(context);
298     final ClassProcessingConsumer classesConsumer = new ClassProcessingConsumer(context, outputSink);
299     try {
300       final boolean rc;
301       if (USE_EMBEDDED_JAVAC) {
302         rc = JavacMain.compile(options, files, classpath, platformCp, sourcePath, outs, diagnosticSink, classesConsumer, context.getCancelStatus());
303       }
304       else {
305         final JavacServerClient client = ensureJavacServerLaunched(context);
306         final RequestFuture<JavacServerResponseHandler> future = client.sendCompileRequest(options, files, classpath, platformCp, sourcePath, outs, diagnosticSink, classesConsumer);
307         try {
308           future.get();
309         }
310         catch (InterruptedException e) {
311           e.printStackTrace(System.err);
312         }
313         catch (ExecutionException e) {
314           e.printStackTrace(System.err);
315         }
316         rc = future.getResponseHandler().isTerminatedSuccessfully();
317       }
318       return rc;
319     }
320     finally {
321       classesConsumer.ensurePendingTasksCompleted();
322     }
323   }
324
325   private static JavacServerClient ensureJavacServerLaunched(CompileContext context) throws Exception {
326     final ExternalJavacDescriptor descriptor = ExternalJavacDescriptor.KEY.get(context);
327     if (descriptor != null) {
328       return descriptor.client;
329     }
330     // start server here
331     final String vmExecPath = System.getProperty(GlobalOptions.VM_EXE_PATH_OPTION, System.getProperty("java.home") + "/bin/java");
332     final String hostString = System.getProperty(GlobalOptions.HOSTNAME_OPTION, "localhost");
333     final int port = findFreePort();
334     final int heapSize = getJavacServerHeapSize(context);
335
336     final BaseOSProcessHandler processHandler = JavacServerBootstrap.launchJavacServer(
337       vmExecPath, heapSize, port, Paths.getSystemRoot(), getCompilationVMOptions(context)
338     );
339     final JavacServerClient client = new JavacServerClient();
340     try {
341       client.connect(hostString, port);
342     }
343     catch (Throwable ex) {
344       processHandler.destroyProcess();
345       throw new Exception("Failed to connect to external javac process: ", ex);
346     }
347     ExternalJavacDescriptor.KEY.set(context, new ExternalJavacDescriptor(processHandler, client));
348     return client;
349   }
350
351   private static int findFreePort() {
352     try {
353       final ServerSocket serverSocket = new ServerSocket(0);
354       try {
355         return serverSocket.getLocalPort();
356       }
357       finally {
358         //workaround for linux : calling close() immediately after opening socket
359         //may result that socket is not closed
360         synchronized(serverSocket) {
361           try {
362             serverSocket.wait(1);
363           }
364           catch (Throwable ignored) {
365           }
366         }
367         serverSocket.close();
368       }
369     }
370     catch (IOException e) {
371       e.printStackTrace(System.err);
372       return JavacServer.DEFAULT_SERVER_PORT;
373     }
374   }
375
376   private static int getJavacServerHeapSize(CompileContext context) {
377     int heapSize = 512;
378     final Project project = context.getProject();
379     final Map<String, String> javacOpts = project.getCompilerConfiguration().getJavacOptions();
380     final String hSize = javacOpts.get("MAXIMUM_HEAP_SIZE");
381     if (hSize != null) {
382       try {
383         heapSize = Integer.parseInt(hSize);
384       }
385       catch (NumberFormatException ignored) {
386       }
387     }
388     return heapSize;
389   }
390
391   private static ClassLoader createInstrumentationClassLoader(Collection<File> classpath, Collection<File> platformCp, Map<File, String> chunkSourcePath, OutputFilesSink outputSink)
392     throws MalformedURLException {
393     final List<URL> urls = new ArrayList<URL>();
394     for (Collection<File> cp : Arrays.asList(platformCp, classpath)) {
395       for (File file : cp) {
396         urls.add(file.toURI().toURL());
397       }
398     }
399     urls.add(getResourcePath(GridConstraints.class).toURI().toURL()); // forms_rt.jar
400     //urls.add(getResourcePath(CellConstraints.class).toURI().toURL());  // jgoodies-forms
401     for (File file : chunkSourcePath.keySet()) {
402       urls.add(file.toURI().toURL());
403     }
404     return new CompiledClassesLoader(outputSink, urls.toArray(new URL[urls.size()]));
405   }
406
407   private static final Key<List<String>> JAVAC_OPTIONS = Key.create("_javac_options_");
408   private static final Key<List<String>> JAVAC_VM_OPTIONS = Key.create("_javac_vm_options_");
409
410   private static List<String> getCompilationVMOptions(CompileContext context) {
411     List<String> cached = JAVAC_VM_OPTIONS.get(context);
412     if (cached == null) {
413       loadJavacOptions(context);
414       cached = JAVAC_VM_OPTIONS.get(context);
415     }
416     return cached;
417   }
418
419   private static List<String> getCompilationOptions(CompileContext context) {
420     List<String> cached = JAVAC_OPTIONS.get(context);
421     if (cached == null) {
422       loadJavacOptions(context);
423       cached = JAVAC_OPTIONS.get(context);
424     }
425     return cached;
426   }
427
428   private static void loadJavacOptions(CompileContext context) {
429     final List<String> options = new ArrayList<String>();
430     final List<String> vmOptions = new ArrayList<String>();
431
432     options.add("-verbose");
433
434     final Project project = context.getProject();
435     final Map<String, String> javacOpts = project.getCompilerConfiguration().getJavacOptions();
436     final boolean debugInfo = !"false".equals(javacOpts.get("DEBUGGING_INFO"));
437     final boolean nowarn = "true".equals(javacOpts.get("GENERATE_NO_WARNINGS"));
438     final boolean deprecation = !"false".equals(javacOpts.get("DEPRECATION"));
439     if (debugInfo) {
440       options.add("-g");
441     }
442     if (deprecation) {
443       options.add("-deprecation");
444     }
445     if (nowarn) {
446       options.add("-nowarn");
447     }
448
449     final String customArgs = javacOpts.get("ADDITIONAL_OPTIONS_STRING");
450     boolean isEncodingSet = false;
451     if (customArgs != null) {
452       final StringTokenizer tokenizer = new StringTokenizer(customArgs, " \t\r\n");
453       while(tokenizer.hasMoreTokens()) {
454         final String token = tokenizer.nextToken();
455         if ("-g".equals(token) || "-deprecation".equals(token) || "-nowarn".equals(token) || "-verbose".equals(token)){
456           continue;
457         }
458         if (token.startsWith("-J-")) {
459           vmOptions.add(token.substring("-J".length()));
460         }
461         else {
462           options.add(token);
463         }
464         if ("-encoding".equals(token)) {
465           isEncodingSet = true;
466         }
467       }
468     }
469
470     if (!isEncodingSet && !StringUtil.isEmpty(project.getProjectCharset())) {
471       options.add("-encoding");
472       options.add(project.getProjectCharset());
473     }
474
475     JAVAC_OPTIONS.set(context, options);
476     JAVAC_VM_OPTIONS.set(context, vmOptions);
477   }
478
479   private static Map<File, Set<File>> buildOutputDirectoriesMap(CompileContext context, ModuleChunk chunk) {
480     final Map<File, Set<File>> map = new HashMap<File, Set<File>>();
481     final boolean compilingTests = context.isCompilingTests();
482     for (Module module : chunk.getModules()) {
483       final String outputPath;
484       final Collection<String> srcPaths;
485       if (compilingTests) {
486         outputPath = module.getTestOutputPath();
487         srcPaths = module.getTestRoots();
488       }
489       else {
490         outputPath = module.getOutputPath();
491         srcPaths = module.getSourceRoots();
492       }
493       final Set<File> roots = new HashSet<File>();
494       for (String path : srcPaths) {
495         roots.add(new File(path));
496       }
497       map.put(new File(outputPath), roots);
498     }
499     return map;
500   }
501
502   // todo: probably instrument other NotNull-like annotations defined in project settings?
503   private static void instrumentNotNull(CompileContext context, OutputFilesSink sink, final ClassLoader loader) {
504     for (final OutputFileObject fileObject : sink.getFileObjects()) {
505       final OutputFileObject.Content originalContent = fileObject.getContent();
506       final ClassReader reader = new ClassReader(originalContent.getBuffer(), originalContent.getOffset(), originalContent.getLength());
507       final int version = getClassFileVersion(reader);
508       if (version >= Opcodes.V1_5) {
509         boolean success = false;
510         final ClassWriter writer = new InstrumenterClassWriter(getAsmClassWriterFlags(version), loader);
511         try {
512           final NotNullVerifyingInstrumenter instrumenter = new NotNullVerifyingInstrumenter(writer);
513           reader.accept(instrumenter, 0);
514           if (instrumenter.isModification()) {
515             fileObject.updateContent(writer.toByteArray());
516           }
517           success = true;
518         }
519         catch (Throwable e) {
520           final StringBuilder msg = new StringBuilder();
521           msg.append("@NotNull instrumentation failed ");
522           final File sourceFile = fileObject.getSourceFile();
523           if (sourceFile != null) {
524             msg.append(" for ").append(sourceFile.getName());
525           }
526           msg.append(": ").append(e.getMessage());
527           context.processMessage(
528             new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, msg.toString(), sourceFile != null ? sourceFile.getPath() : null));
529         }
530         finally {
531           if (!success) {
532             sink.markError(fileObject);
533           }
534         }
535       }
536     }
537   }
538
539   private static void instrumentForms(
540     CompileContext context,
541     ModuleChunk chunk,
542     final Map<File, String> chunkSourcePath,
543     final ClassLoader loader,
544     Collection<File> formsToInstrument,
545     OutputFilesSink outputSink) throws ProjectBuildException {
546
547     final Map<String, File> class2form = new HashMap<String, File>();
548     final SourceToFormMapping sourceToFormMap = context.getDataManager().getSourceToFormMap();
549
550     final Map<String, OutputFileObject> compiledClassNames = new HashMap<String, OutputFileObject>();
551     for (OutputFileObject fileObject : outputSink.getFileObjects()) {
552       compiledClassNames.put(fileObject.getClassName(), fileObject);
553     }
554
555     final MyNestedFormLoader nestedFormsLoader = new MyNestedFormLoader(
556       chunkSourcePath, ProjectPaths.getOutputPathsWithDependents(chunk, context.isCompilingTests())
557     );
558
559     for (File formFile : formsToInstrument) {
560       final LwRootContainer rootContainer;
561       try {
562         rootContainer = Utils.getRootContainer(formFile.toURI().toURL(), new CompiledClassPropertiesProvider(loader));
563       }
564       catch (AlienFormFileException e) {
565         // ignore non-IDEA forms
566         continue;
567       }
568       catch (Exception e) {
569         throw new ProjectBuildException("Cannot process form file " + formFile.getAbsolutePath(), e);
570       }
571
572       final String classToBind = rootContainer.getClassToBind();
573       if (classToBind == null) {
574         continue;
575       }
576
577       final OutputFileObject outputClassFile = findClassFile(compiledClassNames, classToBind);
578       if (outputClassFile == null) {
579         context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.WARNING, "Class to bind does not exist: " + classToBind, formFile.getAbsolutePath()));
580         continue;
581       }
582
583       final File alreadyProcessedForm = class2form.get(classToBind);
584       if (alreadyProcessedForm != null) {
585         context.processMessage(new CompilerMessage(
586           BUILDER_NAME, BuildMessage.Kind.WARNING,
587           formFile.getAbsolutePath() + ": The form is bound to the class " + classToBind + ".\nAnother form " + alreadyProcessedForm.getAbsolutePath() + " is also bound to this class",
588           formFile.getAbsolutePath())
589         );
590         continue;
591       }
592
593       class2form.put(classToBind, formFile);
594
595       boolean success = true;
596       try {
597         final OutputFileObject.Content originalContent = outputClassFile.getContent();
598         final ClassReader classReader = new ClassReader(originalContent.getBuffer(), originalContent.getOffset(), originalContent.getLength());
599
600         final int version = getClassFileVersion(classReader);
601         final InstrumenterClassWriter classWriter = new InstrumenterClassWriter(getAsmClassWriterFlags(version), loader);
602         final AsmCodeGenerator codeGenerator = new AsmCodeGenerator(rootContainer, loader, nestedFormsLoader, false, classWriter);
603         final byte[] patchedBytes = codeGenerator.patchClass(classReader);
604         if (patchedBytes != null) {
605           outputClassFile.updateContent(patchedBytes);
606         }
607
608         final FormErrorInfo[] warnings = codeGenerator.getWarnings();
609         for (final FormErrorInfo warning : warnings) {
610           context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.WARNING, warning.getErrorMessage(), formFile.getAbsolutePath()));
611         }
612
613         final FormErrorInfo[] errors = codeGenerator.getErrors();
614         if (errors.length > 0) {
615           success = false;
616           StringBuilder message = new StringBuilder();
617           for (final FormErrorInfo error : errors) {
618             if (message.length() > 0) {
619               message.append("\n");
620             }
621             message.append(formFile.getAbsolutePath()).append(": ").append(error.getErrorMessage());
622           }
623           context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, message.toString()));
624         }
625         else {
626           final File sourceFile = outputClassFile.getSourceFile();
627           if (sourceFile != null) {
628             sourceToFormMap.update(sourceFile.getPath(), formFile.getPath());
629           }
630         }
631       }
632       catch (Exception e) {
633         success = false;
634         context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, "Forms instrumentation failed" + e.getMessage(), formFile.getAbsolutePath()));
635       }
636       finally {
637         if (!success) {
638           outputSink.markError(outputClassFile);
639         }
640       }
641     }
642   }
643
644   private static OutputFileObject findClassFile(Map<String, OutputFileObject> outputs, String classToBind) {
645     while (true) {
646       final OutputFileObject fo = outputs.get(classToBind);
647       if (fo != null) {
648         return fo;
649       }
650       final int dotIndex = classToBind.lastIndexOf('.');
651       if (dotIndex <= 0) {
652         return null;
653       }
654       classToBind = classToBind.substring(0, dotIndex) + "$" + classToBind.substring(dotIndex + 1);
655     }
656   }
657
658   private static int getClassFileVersion(ClassReader reader) {
659     final Ref<Integer> result = new Ref<Integer>(0);
660     reader.accept(new EmptyVisitor() {
661       public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
662         result.set(version);
663       }
664     }, 0);
665     return result.get();
666   }
667
668   private static int getAsmClassWriterFlags(int version) {
669     return version >= Opcodes.V1_6 && version != Opcodes.V1_1 ? ClassWriter.COMPUTE_FRAMES : ClassWriter.COMPUTE_MAXS;
670   }
671
672   private static class DiagnosticSink implements DiagnosticOutputConsumer {
673     private final CompileContext myContext;
674     private volatile int myErrorCount = 0;
675     private volatile int myWarningCount = 0;
676
677     public DiagnosticSink(CompileContext context) {
678       myContext = context;
679     }
680
681     public void outputLineAvailable(String line) {
682       if (line != null) {
683         //System.err.println(line);
684         if (line.startsWith("[") && line.endsWith("]")) {
685           final String message = line.substring(1, line.length() - 1);
686           if (message.startsWith("parsing")) {
687             myContext.processMessage(new ProgressMessage("Parsing sources..."));
688           }
689           else {
690             if (!message.startsWith("total")) {
691               myContext.processMessage(new ProgressMessage(FileUtil.toSystemDependentName(message)));
692             }
693           }
694         }
695         else if (line.contains("java.lang.OutOfMemoryError")) {
696           myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, "OutOfMemoryError: insufficient memory"));
697         }
698       }
699     }
700
701     public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
702       final CompilerMessage.Kind kind;
703       switch (diagnostic.getKind()) {
704         case ERROR:
705           kind = BuildMessage.Kind.ERROR;
706           myErrorCount++;
707           break;
708         case MANDATORY_WARNING:
709         case WARNING:
710         case NOTE:
711           kind = BuildMessage.Kind.WARNING;
712           myWarningCount++;
713           break;
714         default:
715           kind = BuildMessage.Kind.INFO;
716       }
717       final JavaFileObject source = diagnostic.getSource();
718       final File sourceFile = source != null? Paths.convertToFile(source.toUri()) : null;
719       final String srcPath = sourceFile != null? FileUtil.toSystemIndependentName(sourceFile.getPath()) : null;
720       myContext.processMessage(new CompilerMessage(
721         BUILDER_NAME, kind, diagnostic.getMessage(Locale.US), srcPath,
722         diagnostic.getStartPosition(), diagnostic.getEndPosition(), diagnostic.getPosition(),
723         diagnostic.getLineNumber(), diagnostic.getColumnNumber()
724       ));
725     }
726
727     public int getErrorCount() {
728       return myErrorCount;
729     }
730
731     public int getWarningCount() {
732       return myWarningCount;
733     }
734   }
735
736   private static class OutputFilesSink implements OutputFileConsumer {
737     private final CompileContext myContext;
738     private final Set<File> mySuccessfullyCompiled = new HashSet<File>();
739     private final Set<File> myProblematic = new HashSet<File>();
740     private final List<OutputFileObject> myFileObjects = new ArrayList<OutputFileObject>();
741     private final Map<String, OutputFileObject> myCompiledClasses = new HashMap<String, OutputFileObject>();
742
743     public OutputFilesSink(CompileContext context) {
744       myContext = context;
745     }
746
747     public void save(final @NotNull OutputFileObject fileObject) {
748       final String className = fileObject.getClassName();
749       if (className != null) {
750         final OutputFileObject.Content content = fileObject.getContent();
751         if (content != null) {
752           synchronized (myCompiledClasses) {
753             myCompiledClasses.put(className, fileObject);
754           }
755         }
756       }
757
758       synchronized (myFileObjects) {
759         myFileObjects.add(fileObject);
760       }
761     }
762
763     @Nullable
764     public OutputFileObject.Content lookupClassBytes(String className) {
765       synchronized (myCompiledClasses) {
766         final OutputFileObject object = myCompiledClasses.get(className);
767         return object != null? object.getContent() : null;
768       }
769     }
770
771     public List<OutputFileObject> getFileObjects() {
772       return Collections.unmodifiableList(myFileObjects);
773     }
774
775     public void writePendingData() {
776       try {
777         if (!myFileObjects.isEmpty()) {
778           final FileGeneratedEvent event = new FileGeneratedEvent();
779           try {
780             for (OutputFileObject fileObject : myFileObjects) {
781               try {
782                 writeToDisk(fileObject);
783                 final File rootFile = fileObject.getOutputRoot();
784                 if (rootFile != null) {
785                   event.add(rootFile.getPath(), fileObject.getRelativePath());
786                 }
787               }
788               catch (IOException e) {
789                 myContext.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, e.getMessage()));
790               }
791             }
792           }
793           finally {
794             myContext.processMessage(event);
795           }
796         }
797       }
798       finally {
799         myFileObjects.clear();
800         myCompiledClasses.clear();
801       }
802     }
803
804     public Set<File> getSuccessfullyCompiled() {
805       return Collections.unmodifiableSet(mySuccessfullyCompiled);
806     }
807
808     private void writeToDisk(@NotNull OutputFileObject fileObject) throws IOException {
809       final OutputFileObject.Content content = fileObject.getContent();
810       if (content != null) {
811         FileUtil.writeToFile(fileObject.getFile(), content.getBuffer(), content.getOffset(), content.getLength());
812       }
813       else {
814         throw new IOException("Missing content for file " + fileObject.getFile());
815       }
816
817       final File source = fileObject.getSourceFile();
818       if (source != null && !myProblematic.contains(source)) {
819         mySuccessfullyCompiled.add(source);
820         final String className = fileObject.getClassName();
821         if (className != null) {
822           myContext.processMessage(new ProgressMessage("Compiled " + className));
823         }
824       }
825
826     }
827
828     public void markError(OutputFileObject outputClassFile) {
829       final File source = outputClassFile.getSourceFile();
830       if (source != null) {
831         myProblematic.add(source);
832       }
833     }
834   }
835
836   public static class InstrumenterClassWriter extends ClassWriter {
837     private final ClassLoader myClassLoader;
838
839     public InstrumenterClassWriter(int flags, final ClassLoader pseudoLoader) {
840       super(flags);
841       myClassLoader = pseudoLoader;
842     }
843
844     protected String getCommonSuperClass(final String type1, final String type2) {
845       Class c, d;
846       try {
847         //c = Class.forName(type1.replace('/', '.'), true, myClassLoader);
848         //d = Class.forName(type2.replace('/', '.'), true, myClassLoader);
849         c = myClassLoader.loadClass(type1.replace('/', '.'));
850         d = myClassLoader.loadClass(type2.replace('/', '.'));
851       }
852       catch (Exception e) {
853         throw new RuntimeException(e.toString(), e);
854       }
855       if (c.isAssignableFrom(d)) {
856         return type1;
857       }
858       if (d.isAssignableFrom(c)) {
859         return type2;
860       }
861       if (c.isInterface() || d.isInterface()) {
862         return "java/lang/Object";
863       }
864       else {
865         do {
866           c = c.getSuperclass();
867         }
868         while (!c.isAssignableFrom(d));
869
870         return c.getName().replace('.', '/');
871       }
872     }
873   }
874
875   private static class MyNestedFormLoader implements NestedFormLoader {
876     private final Map<File, String> mySourceRoots;
877     private final Collection<File> myOutputRoots;
878     private final HashMap<String, LwRootContainer> myCache = new HashMap<String, LwRootContainer>();
879
880     /**
881      * @param sourceRoots all source roots for current module chunk and all dependent recursively
882      * @param outputRoots output roots for this module chunk and all dependent recursively
883      */
884     public MyNestedFormLoader(Map<File, String> sourceRoots, Collection<File> outputRoots) {
885       mySourceRoots = sourceRoots;
886       myOutputRoots = outputRoots;
887     }
888
889     public LwRootContainer loadForm(String formFileName) throws Exception {
890       if (myCache.containsKey(formFileName)) {
891         return myCache.get(formFileName);
892       }
893
894       final String relPath = FileUtil.toSystemIndependentName(formFileName);
895
896       for (Map.Entry<File, String> entry : mySourceRoots.entrySet()) {
897         final File sourceRoot = entry.getKey();
898         final String prefix = entry.getValue();
899         String path = relPath;
900         if (prefix != null && FileUtil.startsWith(path, prefix)) {
901           path = path.substring(prefix.length());
902         }
903         final File formFile = new File(sourceRoot, path);
904         if (formFile.exists()) {
905           final BufferedInputStream stream = new BufferedInputStream(new FileInputStream(formFile));
906           try {
907             return loadForm(formFileName, stream);
908           }
909           finally {
910             stream.close();
911           }
912         }
913       }
914
915       throw new Exception("Cannot find nested form file " + formFileName);
916     }
917
918     private LwRootContainer loadForm(String formFileName, InputStream resourceStream) throws Exception {
919       final LwRootContainer container = Utils.getRootContainer(resourceStream, null);
920       myCache.put(formFileName, container);
921       return container;
922     }
923
924     public String getClassToBindName(LwRootContainer container) {
925       final String className = container.getClassToBind();
926       for (File outputRoot : myOutputRoots) {
927         final String result = getJVMClassName(outputRoot, className.replace('.', '/'));
928         if (result != null) {
929           return result.replace('/', '.');
930         }
931       }
932       return className;
933     }
934   }
935
936   @Nullable
937   private static String getJVMClassName(File outputRoot, String className) {
938     while (true) {
939       final File candidateClass = new File(outputRoot, className + ".class");
940       if (candidateClass.exists()) {
941         return className;
942       }
943       final int position = className.lastIndexOf('/');
944       if (position < 0) {
945         return null;
946       }
947       className = className.substring(0, position) + '$' + className.substring(position + 1);
948     }
949   }
950
951   private static File getResourcePath(Class aClass) {
952     return new File(PathManager.getResourceRoot(aClass, "/" + aClass.getName().replace('.', '/') + ".class"));
953   }
954
955   private static class CompiledClassesLoader extends URLClassLoader {
956     private final OutputFilesSink mySink;
957
958     public CompiledClassesLoader(OutputFilesSink sink, URL[] urls) {
959       super(urls, null);
960       mySink = sink;
961     }
962
963     protected Class findClass(String name) throws ClassNotFoundException {
964       final OutputFileObject.Content content = mySink.lookupClassBytes(name);
965       if (content != null) {
966         return defineClass(name, content.getBuffer(), content.getOffset(), content.getLength());
967       }
968       return super.findClass(name);
969     }
970
971     public URL findResource(String name) {
972       return super.findResource(name);
973     }
974   }
975
976   private class ClassProcessingConsumer implements OutputFileConsumer {
977     private final CompileContext myCompileContext;
978     private final OutputFileConsumer myDelegateOutputFileSink;
979     private int myTasksInProgress = 0;
980     private final Object myCounterLock = new Object();
981
982     public ClassProcessingConsumer(CompileContext compileContext, OutputFileConsumer sink) {
983       myCompileContext = compileContext;
984       myDelegateOutputFileSink = sink != null? sink : new OutputFileConsumer() {
985         public void save(@NotNull OutputFileObject fileObject) {
986           throw new RuntimeException("Output sink for compiler was not specified");
987         }
988       };
989     }
990
991     public void save(@NotNull final OutputFileObject fileObject) {
992       incTaskCount();
993       myTaskRunner.submit(new Runnable() {
994         public void run() {
995           try {
996             for (ClassPostProcessor processor : myClassProcessors) {
997               processor.process(myCompileContext, fileObject);
998             }
999           }
1000           finally {
1001             try {
1002               myDelegateOutputFileSink.save(fileObject);
1003             }
1004             finally {
1005               decTaskCount();
1006             }
1007           }
1008         }
1009       });
1010     }
1011
1012     private void decTaskCount() {
1013       synchronized (myCounterLock) {
1014         myTasksInProgress = Math.max(0, myTasksInProgress - 1);
1015         if (myTasksInProgress == 0) {
1016           myCounterLock.notifyAll();
1017         }
1018       }
1019     }
1020
1021     private void incTaskCount() {
1022       synchronized (myCounterLock) {
1023         myTasksInProgress++;
1024       }
1025     }
1026
1027     public void ensurePendingTasksCompleted() {
1028       synchronized (myCounterLock) {
1029         while (myTasksInProgress > 0) {
1030           try {
1031             myCounterLock.wait();
1032           }
1033           catch (InterruptedException ignored) {
1034           }
1035         }
1036       }
1037     }
1038
1039   }
1040 }