@Override
 public void buildStarted(CompileContext context) {
   final JpsProject project = context.getProjectDescriptor().getProject();
   final JpsJavaCompilerConfiguration config =
       JpsJavaExtensionService.getInstance().getCompilerConfiguration(project);
   final String compilerId = config == null ? JavaCompilers.JAVAC_ID : config.getJavaCompilerId();
   if (LOG.isDebugEnabled()) {
     LOG.debug("Java compiler ID: " + compilerId);
   }
   final boolean isJavac =
       JavaCompilers.JAVAC_ID.equalsIgnoreCase(compilerId)
           || JavaCompilers.JAVAC_API_ID.equalsIgnoreCase(compilerId);
   final boolean isEclipse =
       JavaCompilers.ECLIPSE_ID.equalsIgnoreCase(compilerId)
           || JavaCompilers.ECLIPSE_EMBEDDED_ID.equalsIgnoreCase(compilerId);
   IS_ENABLED.set(context, isJavac || isEclipse);
   String messageText = null;
   if (isJavac) {
     messageText =
         "Using javac " + System.getProperty("java.version") + " to compile java sources";
   } else if (isEclipse) {
     messageText = "Using eclipse compiler to compile java sources";
   }
   COMPILER_VERSION_INFO.set(context, new AtomicReference<String>(messageText));
 }
  public ModuleLevelBuilder.ExitCode build(
      final CompileContext context,
      final ModuleChunk chunk,
      DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
      OutputConsumer outputConsumer)
      throws ProjectBuildException {
    if (GreclipseBuilder.useGreclipse(context)) return ExitCode.NOTHING_DONE;

    long start = 0;
    try {
      JpsGroovySettings settings =
          JpsGroovySettings.getSettings(context.getProjectDescriptor().getProject());

      Ref<Boolean> hasStubExcludes = Ref.create(false);
      final List<File> toCompile =
          collectChangedFiles(context, dirtyFilesHolder, myForStubs, false, hasStubExcludes);
      if (toCompile.isEmpty()) {
        return hasFilesToCompileForNextRound(context)
            ? ExitCode.ADDITIONAL_PASS_REQUIRED
            : ExitCode.NOTHING_DONE;
      }
      if (Utils.IS_TEST_MODE || LOG.isDebugEnabled()) {
        LOG.info("forStubs=" + myForStubs);
      }

      Map<ModuleBuildTarget, String> finalOutputs = getCanonicalModuleOutputs(context, chunk, this);
      if (finalOutputs == null) {
        return ExitCode.ABORT;
      }

      start = System.currentTimeMillis();

      Map<ModuleBuildTarget, String> generationOutputs =
          myForStubs ? getStubGenerationOutputs(chunk, context) : finalOutputs;
      String compilerOutput = generationOutputs.get(chunk.representativeTarget());

      GroovycOutputParser parser =
          runGroovycOrContinuation(
              context,
              chunk,
              settings,
              finalOutputs,
              compilerOutput,
              toCompile,
              hasStubExcludes.get());

      Map<ModuleBuildTarget, Collection<GroovycOutputParser.OutputItem>> compiled =
          processCompiledFiles(
              context, chunk, generationOutputs, compilerOutput, parser.getSuccessfullyCompiled());

      if (checkChunkRebuildNeeded(context, parser)) {
        clearContinuation(context, chunk);
        return ExitCode.CHUNK_REBUILD_REQUIRED;
      }

      if (myForStubs) {
        addStubRootsToJavacSourcePath(context, generationOutputs);
        rememberStubSources(context, compiled);
      }

      for (CompilerMessage message : parser.getCompilerMessages()) {
        context.processMessage(message);
      }

      if (!myForStubs) {
        updateDependencies(context, toCompile, compiled, outputConsumer, this);
      }
      return hasFilesToCompileForNextRound(context)
          ? ExitCode.ADDITIONAL_PASS_REQUIRED
          : ExitCode.OK;
    } catch (Exception e) {
      throw new ProjectBuildException(e);
    } finally {
      if (start > 0 && LOG.isDebugEnabled()) {
        LOG.debug(
            myBuilderName
                + " took "
                + (System.currentTimeMillis() - start)
                + " on "
                + chunk.getName());
      }
      if (!myForStubs) {
        FILES_MARKED_DIRTY_FOR_NEXT_ROUND.set(context, null);
      }
    }
  }
/** @author Eugene Zhuravlev Date: 10/25/11 */
public class GroovyBuilder extends ModuleLevelBuilder {
  private static final int ourOptimizeThreshold =
      Integer.parseInt(System.getProperty("groovyc.optimized.class.loading.threshold", "10"));
  private static final Logger LOG =
      Logger.getInstance("#org.jetbrains.jps.incremental.groovy.GroovyBuilder");
  private static final Key<Boolean> CHUNK_REBUILD_ORDERED = Key.create("CHUNK_REBUILD_ORDERED");
  private static final Key<Map<String, String>> STUB_TO_SRC = Key.create("STUB_TO_SRC");
  private static final Key<Map<ModuleChunk, GroovycContinuation>> CONTINUATIONS =
      Key.create("CONTINUATIONS");
  private static final Key<Boolean> FILES_MARKED_DIRTY_FOR_NEXT_ROUND =
      Key.create("SRC_MARKED_DIRTY");
  private static final String GROOVY_EXTENSION = "groovy";
  private final boolean myForStubs;
  private final String myBuilderName;

  public GroovyBuilder(boolean forStubs) {
    super(forStubs ? BuilderCategory.SOURCE_GENERATOR : BuilderCategory.OVERWRITING_TRANSLATOR);
    myForStubs = forStubs;
    myBuilderName = "Groovy " + (forStubs ? "stub generator" : "compiler");
  }

  static {
    JavaBuilder.registerClassPostProcessor(new RecompileStubSources());
  }

  public ModuleLevelBuilder.ExitCode build(
      final CompileContext context,
      final ModuleChunk chunk,
      DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
      OutputConsumer outputConsumer)
      throws ProjectBuildException {
    if (GreclipseBuilder.useGreclipse(context)) return ExitCode.NOTHING_DONE;

    long start = 0;
    try {
      JpsGroovySettings settings =
          JpsGroovySettings.getSettings(context.getProjectDescriptor().getProject());

      Ref<Boolean> hasStubExcludes = Ref.create(false);
      final List<File> toCompile =
          collectChangedFiles(context, dirtyFilesHolder, myForStubs, false, hasStubExcludes);
      if (toCompile.isEmpty()) {
        return hasFilesToCompileForNextRound(context)
            ? ExitCode.ADDITIONAL_PASS_REQUIRED
            : ExitCode.NOTHING_DONE;
      }
      if (Utils.IS_TEST_MODE || LOG.isDebugEnabled()) {
        LOG.info("forStubs=" + myForStubs);
      }

      Map<ModuleBuildTarget, String> finalOutputs = getCanonicalModuleOutputs(context, chunk, this);
      if (finalOutputs == null) {
        return ExitCode.ABORT;
      }

      start = System.currentTimeMillis();

      Map<ModuleBuildTarget, String> generationOutputs =
          myForStubs ? getStubGenerationOutputs(chunk, context) : finalOutputs;
      String compilerOutput = generationOutputs.get(chunk.representativeTarget());

      GroovycOutputParser parser =
          runGroovycOrContinuation(
              context,
              chunk,
              settings,
              finalOutputs,
              compilerOutput,
              toCompile,
              hasStubExcludes.get());

      Map<ModuleBuildTarget, Collection<GroovycOutputParser.OutputItem>> compiled =
          processCompiledFiles(
              context, chunk, generationOutputs, compilerOutput, parser.getSuccessfullyCompiled());

      if (checkChunkRebuildNeeded(context, parser)) {
        clearContinuation(context, chunk);
        return ExitCode.CHUNK_REBUILD_REQUIRED;
      }

      if (myForStubs) {
        addStubRootsToJavacSourcePath(context, generationOutputs);
        rememberStubSources(context, compiled);
      }

      for (CompilerMessage message : parser.getCompilerMessages()) {
        context.processMessage(message);
      }

      if (!myForStubs) {
        updateDependencies(context, toCompile, compiled, outputConsumer, this);
      }
      return hasFilesToCompileForNextRound(context)
          ? ExitCode.ADDITIONAL_PASS_REQUIRED
          : ExitCode.OK;
    } catch (Exception e) {
      throw new ProjectBuildException(e);
    } finally {
      if (start > 0 && LOG.isDebugEnabled()) {
        LOG.debug(
            myBuilderName
                + " took "
                + (System.currentTimeMillis() - start)
                + " on "
                + chunk.getName());
      }
      if (!myForStubs) {
        FILES_MARKED_DIRTY_FOR_NEXT_ROUND.set(context, null);
      }
    }
  }

  @NotNull
  private GroovycOutputParser runGroovycOrContinuation(
      CompileContext context,
      ModuleChunk chunk,
      JpsGroovySettings settings,
      Map<ModuleBuildTarget, String> finalOutputs,
      String compilerOutput,
      List<File> toCompile,
      boolean hasStubExcludes)
      throws Exception {
    GroovycContinuation continuation = takeContinuation(context, chunk);
    if (continuation != null) {
      if (Utils.IS_TEST_MODE || LOG.isDebugEnabled()) {
        LOG.info("using continuation for " + chunk);
      }
      return continuation.continueCompilation();
    }

    final Set<String> toCompilePaths = getPathsToCompile(toCompile);

    JpsSdk<JpsDummyElement> jdk = getJdk(chunk);
    String version = jdk == null ? SystemInfo.JAVA_RUNTIME_VERSION : jdk.getVersionString();
    boolean inProcess = "true".equals(System.getProperty("groovyc.in.process", "true"));
    boolean mayDependOnUtilJar =
        version != null && StringUtil.compareVersionNumbers(version, "1.6") >= 0;
    boolean optimizeClassLoading =
        !inProcess
            && mayDependOnUtilJar
            && ourOptimizeThreshold != 0
            && toCompilePaths.size() >= ourOptimizeThreshold;

    Map<String, String> class2Src =
        buildClassToSourceMap(chunk, context, toCompilePaths, finalOutputs);

    final String encoding =
        context
            .getProjectDescriptor()
            .getEncodingConfiguration()
            .getPreferredModuleChunkEncoding(chunk);
    List<String> patchers = new ArrayList<String>();

    for (GroovyBuilderExtension extension :
        JpsServiceManager.getInstance().getExtensions(GroovyBuilderExtension.class)) {
      patchers.addAll(extension.getCompilationUnitPatchers(context, chunk));
    }

    Collection<String> classpath = generateClasspath(context, chunk);
    if (LOG.isDebugEnabled()) {
      LOG.debug("Optimized class loading: " + optimizeClassLoading);
      LOG.debug("Groovyc classpath: " + classpath);
    }

    final File tempFile =
        GroovycOutputParser.fillFileWithGroovycParameters(
            compilerOutput,
            toCompilePaths,
            finalOutputs.values(),
            class2Src,
            encoding,
            patchers,
            optimizeClassLoading ? StringUtil.join(classpath, File.pathSeparator) : "");
    GroovycFlavor groovyc =
        inProcess
            ? new InProcessGroovyc(finalOutputs.values(), hasStubExcludes)
            : new ForkedGroovyc(optimizeClassLoading, chunk);

    GroovycOutputParser parser = new GroovycOutputParser(chunk, context);

    continuation = groovyc.runGroovyc(classpath, myForStubs, settings, tempFile, parser);
    setContinuation(context, chunk, continuation);
    return parser;
  }

  private static void clearContinuation(CompileContext context, ModuleChunk chunk) {
    GroovycContinuation continuation = takeContinuation(context, chunk);
    if (continuation != null) {
      if (Utils.IS_TEST_MODE || LOG.isDebugEnabled()) {
        LOG.info("clearing continuation for " + chunk);
      }
      continuation.buildAborted();
    }
  }

  @Nullable
  private static GroovycContinuation takeContinuation(CompileContext context, ModuleChunk chunk) {
    Map<ModuleChunk, GroovycContinuation> map = CONTINUATIONS.get(context);
    return map == null ? null : map.remove(chunk);
  }

  private static void setContinuation(
      CompileContext context, ModuleChunk chunk, @Nullable GroovycContinuation continuation) {
    clearContinuation(context, chunk);
    if (continuation != null) {
      if (Utils.IS_TEST_MODE || LOG.isDebugEnabled()) {
        LOG.info("registering continuation for " + chunk);
      }

      Map<ModuleChunk, GroovycContinuation> map = CONTINUATIONS.get(context);
      if (map == null) CONTINUATIONS.set(context, map = ContainerUtil.newConcurrentMap());
      map.put(chunk, continuation);
    }
  }

  private Boolean hasFilesToCompileForNextRound(CompileContext context) {
    return !myForStubs && FILES_MARKED_DIRTY_FOR_NEXT_ROUND.get(context, Boolean.FALSE);
  }

  private static Set<String> getPathsToCompile(List<File> toCompile) {
    final Set<String> toCompilePaths = new LinkedHashSet<String>();
    for (File file : toCompile) {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Path to compile: " + file.getPath());
      }
      toCompilePaths.add(FileUtil.toSystemIndependentName(file.getPath()));
    }
    return toCompilePaths;
  }

  private static boolean checkChunkRebuildNeeded(
      CompileContext context, GroovycOutputParser parser) {
    if (JavaBuilderUtil.isForcedRecompilationAllJavaModules(context) || !parser.shouldRetry()) {
      return false;
    }

    if (CHUNK_REBUILD_ORDERED.get(context) != null) {
      CHUNK_REBUILD_ORDERED.set(context, null);
      return false;
    }

    CHUNK_REBUILD_ORDERED.set(context, Boolean.TRUE);
    LOG.info("Order chunk rebuild");
    return true;
  }

  private static void rememberStubSources(
      CompileContext context,
      Map<ModuleBuildTarget, Collection<GroovycOutputParser.OutputItem>> compiled) {
    Map<String, String> stubToSrc = STUB_TO_SRC.get(context);
    if (stubToSrc == null) {
      STUB_TO_SRC.set(context, stubToSrc = new HashMap<String, String>());
    }
    for (Collection<GroovycOutputParser.OutputItem> items : compiled.values()) {
      for (GroovycOutputParser.OutputItem item : items) {
        stubToSrc.put(FileUtil.toSystemIndependentName(item.outputPath), item.sourcePath);
      }
    }
  }

  private static void addStubRootsToJavacSourcePath(
      CompileContext context, Map<ModuleBuildTarget, String> generationOutputs) {
    final BuildRootIndex rootsIndex = context.getProjectDescriptor().getBuildRootIndex();
    for (ModuleBuildTarget target : generationOutputs.keySet()) {
      File root = new File(generationOutputs.get(target));
      rootsIndex.associateTempRoot(
          context,
          target,
          new JavaSourceRootDescriptor(root, target, true, true, "", Collections.<File>emptySet()));
    }
  }

  public static Map<ModuleBuildTarget, Collection<GroovycOutputParser.OutputItem>>
      processCompiledFiles(
          CompileContext context,
          ModuleChunk chunk,
          Map<ModuleBuildTarget, String> generationOutputs,
          String compilerOutput,
          List<GroovycOutputParser.OutputItem> successfullyCompiled)
          throws IOException {
    ProjectDescriptor pd = context.getProjectDescriptor();

    final Map<ModuleBuildTarget, Collection<GroovycOutputParser.OutputItem>> compiled =
        new THashMap<ModuleBuildTarget, Collection<GroovycOutputParser.OutputItem>>();
    for (final GroovycOutputParser.OutputItem item : successfullyCompiled) {
      if (Utils.IS_TEST_MODE || LOG.isDebugEnabled()) {
        LOG.info("compiled=" + item);
      }
      final JavaSourceRootDescriptor rd =
          pd.getBuildRootIndex().findJavaRootDescriptor(context, new File(item.sourcePath));
      if (rd != null) {
        final String outputPath =
            ensureCorrectOutput(chunk, item, generationOutputs, compilerOutput, rd.target);

        Collection<GroovycOutputParser.OutputItem> items = compiled.get(rd.target);
        if (items == null) {
          items = new ArrayList<GroovycOutputParser.OutputItem>();
          compiled.put(rd.target, items);
        }

        items.add(new GroovycOutputParser.OutputItem(outputPath, item.sourcePath));
      } else {
        if (Utils.IS_TEST_MODE || LOG.isDebugEnabled()) {
          LOG.info("No java source root descriptor for the item found =" + item);
        }
      }
    }
    if (Utils.IS_TEST_MODE || LOG.isDebugEnabled()) {
      LOG.info("Chunk " + chunk + " compilation finished");
    }
    return compiled;
  }

  @Override
  public void buildStarted(CompileContext context) {
    if (myForStubs) {
      File stubRoot = getStubRoot(context);
      if (stubRoot.exists() && !FileUtil.deleteWithRenaming(stubRoot)) {
        context.processMessage(
            new CompilerMessage(
                myBuilderName,
                BuildMessage.Kind.ERROR,
                "External make cannot clean " + stubRoot.getPath()));
      }
    }
  }

  @Override
  public void chunkBuildFinished(CompileContext context, ModuleChunk chunk) {
    JavaBuilderUtil.cleanupChunkResources(context);
    clearContinuation(context, chunk);
    STUB_TO_SRC.set(context, null);
  }

  private static Map<ModuleBuildTarget, String> getStubGenerationOutputs(
      ModuleChunk chunk, CompileContext context) throws IOException {
    Map<ModuleBuildTarget, String> generationOutputs = new HashMap<ModuleBuildTarget, String>();
    File commonRoot = getStubRoot(context);
    for (ModuleBuildTarget target : chunk.getTargets()) {
      File targetRoot =
          new File(
              commonRoot,
              target.getModule().getName() + File.separator + target.getTargetType().getTypeId());
      if (targetRoot.exists() && !FileUtil.deleteWithRenaming(targetRoot)) {
        throw new IOException("External make cannot clean " + targetRoot.getPath());
      }
      if (!targetRoot.mkdirs()) {
        throw new IOException("External make cannot create " + targetRoot.getPath());
      }
      generationOutputs.put(target, targetRoot.getPath());
    }
    return generationOutputs;
  }

  private static File getStubRoot(CompileContext context) {
    return new File(
        context.getProjectDescriptor().dataManager.getDataPaths().getDataStorageRoot(),
        "groovyStubs");
  }

  @Nullable
  public static Map<ModuleBuildTarget, String> getCanonicalModuleOutputs(
      CompileContext context, ModuleChunk chunk, Builder builder) {
    Map<ModuleBuildTarget, String> finalOutputs = new LinkedHashMap<ModuleBuildTarget, String>();
    for (ModuleBuildTarget target : chunk.getTargets()) {
      File moduleOutputDir = target.getOutputDir();
      if (moduleOutputDir == null) {
        context.processMessage(
            new CompilerMessage(
                builder.getPresentableName(),
                BuildMessage.Kind.ERROR,
                "Output directory not specified for module " + target.getModule().getName()));
        return null;
      }
      //noinspection ResultOfMethodCallIgnored
      moduleOutputDir.mkdirs();
      String moduleOutputPath = FileUtil.toCanonicalPath(moduleOutputDir.getPath());
      assert moduleOutputPath != null;
      finalOutputs.put(
          target, moduleOutputPath.endsWith("/") ? moduleOutputPath : moduleOutputPath + "/");
    }
    return finalOutputs;
  }

  private static String ensureCorrectOutput(
      ModuleChunk chunk,
      GroovycOutputParser.OutputItem item,
      Map<ModuleBuildTarget, String> generationOutputs,
      String compilerOutput,
      @NotNull ModuleBuildTarget srcTarget)
      throws IOException {
    if (chunk.getModules().size() > 1 && !srcTarget.equals(chunk.representativeTarget())) {
      File output = new File(item.outputPath);

      String srcTargetOutput = generationOutputs.get(srcTarget);
      if (srcTargetOutput == null) {
        LOG.info(
            "No output for "
                + srcTarget
                + "; outputs="
                + generationOutputs
                + "; targets = "
                + chunk.getTargets());
        return item.outputPath;
      }

      // todo honor package prefixes
      File correctRoot = new File(srcTargetOutput);
      File correctOutput =
          new File(correctRoot, FileUtil.getRelativePath(new File(compilerOutput), output));

      FileUtil.rename(output, correctOutput);
      return correctOutput.getPath();
    }
    return item.outputPath;
  }

  static JpsSdk<JpsDummyElement> getJdk(ModuleChunk chunk) {
    return chunk.getModules().iterator().next().getSdk(JpsJavaSdkType.INSTANCE);
  }

  static List<File> collectChangedFiles(
      CompileContext context,
      DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
      final boolean forStubs,
      final boolean forEclipse,
      final Ref<Boolean> hasExcludes)
      throws IOException {

    final JpsJavaCompilerConfiguration configuration =
        JpsJavaExtensionService.getInstance()
            .getCompilerConfiguration(context.getProjectDescriptor().getProject());
    assert configuration != null;

    final JpsGroovySettings settings =
        JpsGroovySettings.getSettings(context.getProjectDescriptor().getProject());

    final List<File> toCompile = new ArrayList<File>();
    dirtyFilesHolder.processDirtyFiles(
        new FileProcessor<JavaSourceRootDescriptor, ModuleBuildTarget>() {
          public boolean apply(
              ModuleBuildTarget target, File file, JavaSourceRootDescriptor sourceRoot)
              throws IOException {
            final String path = file.getPath();
            // todo file type check
            if ((isGroovyFile(path) || forEclipse && path.endsWith(".java"))
                && !configuration.isResourceFile(file, sourceRoot.root)) {
              if (forStubs && settings.isExcludedFromStubGeneration(file)) {
                hasExcludes.set(true);
                return true;
              }

              toCompile.add(file);
            }
            return true;
          }
        });
    return toCompile;
  }

  public static void updateDependencies(
      CompileContext context,
      List<File> toCompile,
      Map<ModuleBuildTarget, Collection<GroovycOutputParser.OutputItem>> successfullyCompiled,
      OutputConsumer outputConsumer,
      Builder builder)
      throws IOException {
    JavaBuilderUtil.registerFilesToCompile(context, toCompile);
    if (!successfullyCompiled.isEmpty()) {

      final Callbacks.Backend callback = JavaBuilderUtil.getDependenciesRegistrar(context);

      for (Map.Entry<ModuleBuildTarget, Collection<GroovycOutputParser.OutputItem>> entry :
          successfullyCompiled.entrySet()) {
        final ModuleBuildTarget target = entry.getKey();
        final Collection<GroovycOutputParser.OutputItem> compiled = entry.getValue();
        for (GroovycOutputParser.OutputItem item : compiled) {
          final String sourcePath = FileUtil.toSystemIndependentName(item.sourcePath);
          final String outputPath = FileUtil.toSystemIndependentName(item.outputPath);
          final File outputFile = new File(outputPath);
          final File srcFile = new File(sourcePath);
          try {
            final byte[] bytes = FileUtil.loadFileBytes(outputFile);
            if (Utils.IS_TEST_MODE || LOG.isDebugEnabled()) {
              LOG.info("registerCompiledClass " + outputFile + " from " + srcFile);
            }
            outputConsumer.registerCompiledClass(
                target,
                new CompiledClass(
                    outputFile, srcFile, readClassName(bytes), new BinaryContent(bytes)));
            callback.associate(outputPath, sourcePath, new FailSafeClassReader(bytes));
          } catch (Throwable e) {
            // need this to make sure that unexpected errors in, for example, ASM will not ruin the
            // compilation
            final String message =
                "Class dependency information may be incomplete! Error parsing generated class "
                    + item.outputPath;
            LOG.info(message, e);
            context.processMessage(
                new CompilerMessage(
                    builder.getPresentableName(),
                    BuildMessage.Kind.WARNING,
                    message + "\n" + CompilerMessage.getTextFromThrowable(e),
                    sourcePath));
          }
          JavaBuilderUtil.registerSuccessfullyCompiled(context, srcFile);
        }
      }
    }
  }

  private static String readClassName(byte[] classBytes) throws IOException {
    final Ref<String> nameRef = Ref.create(null);
    new ClassReader(classBytes)
        .accept(
            new ClassVisitor(Opcodes.API_VERSION) {
              public void visit(
                  int version,
                  int access,
                  String name,
                  String signature,
                  String superName,
                  String[] interfaces) {
                nameRef.set(name.replace('/', '.'));
              }
            },
            ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
    return nameRef.get();
  }

  private static Collection<String> generateClasspath(CompileContext context, ModuleChunk chunk) {
    final Set<String> cp = new LinkedHashSet<String>();
    // groovy_rt.jar
    // IMPORTANT! must be the first in classpath
    cp.addAll(getGroovyRtRoots());

    for (File file :
        ProjectPaths.getCompilationClasspathFiles(chunk, chunk.containsTests(), false, false)) {
      cp.add(FileUtil.toCanonicalPath(file.getPath()));
    }

    for (GroovyBuilderExtension extension :
        JpsServiceManager.getInstance().getExtensions(GroovyBuilderExtension.class)) {
      cp.addAll(extension.getCompilationClassPath(context, chunk));
    }

    return cp;
  }

  static List<String> getGroovyRtRoots() {
    File rt = ClasspathBootstrap.getResourceFile(GroovyBuilder.class);
    File constants = ClasspathBootstrap.getResourceFile(GroovyRtConstants.class);
    return Arrays.asList(
        new File(rt.getParentFile(), rt.isFile() ? "groovy_rt.jar" : "groovy_rt").getPath(),
        new File(
                constants.getParentFile(),
                constants.isFile() ? "groovy-rt-constants.jar" : "groovy-rt-constants")
            .getPath());
  }

  public static boolean isGroovyFile(String path) {
    return path.endsWith("." + GROOVY_EXTENSION);
  }

  @Override
  public List<String> getCompilableFileExtensions() {
    return Collections.singletonList(GROOVY_EXTENSION);
  }

  private static Map<String, String> buildClassToSourceMap(
      ModuleChunk chunk,
      CompileContext context,
      Set<String> toCompilePaths,
      Map<ModuleBuildTarget, String> finalOutputs)
      throws IOException {
    final Map<String, String> class2Src = new HashMap<String, String>();
    JpsJavaCompilerConfiguration configuration =
        JpsJavaExtensionService.getInstance()
            .getOrCreateCompilerConfiguration(context.getProjectDescriptor().getProject());
    for (ModuleBuildTarget target : chunk.getTargets()) {
      String moduleOutputPath = finalOutputs.get(target);
      final SourceToOutputMapping srcToOut =
          context.getProjectDescriptor().dataManager.getSourceToOutputMap(target);
      for (String src : srcToOut.getSources()) {
        if (!toCompilePaths.contains(src)
            && isGroovyFile(src)
            && !configuration.getCompilerExcludes().isExcluded(new File(src))) {
          final Collection<String> outs = srcToOut.getOutputs(src);
          if (outs != null) {
            for (String out : outs) {
              if (out.endsWith(".class") && out.startsWith(moduleOutputPath)) {
                final String className =
                    out.substring(moduleOutputPath.length(), out.length() - ".class".length())
                        .replace('/', '.');
                class2Src.put(className, src);
              }
            }
          }
        }
      }
    }
    return class2Src;
  }

  @Override
  public String toString() {
    return myBuilderName;
  }

  @NotNull
  public String getPresentableName() {
    return myBuilderName;
  }

  private static class RecompileStubSources implements ClassPostProcessor {

    public void process(CompileContext context, OutputFileObject out) {
      Map<String, String> stubToSrc = STUB_TO_SRC.get(context);
      if (stubToSrc == null) {
        return;
      }
      File src = out.getSourceFile();
      if (src == null) {
        return;
      }
      String groovy = stubToSrc.get(FileUtil.toSystemIndependentName(src.getPath()));
      if (groovy == null) {
        return;
      }
      try {
        final File groovyFile = new File(groovy);
        if (!FSOperations.isMarkedDirty(context, CompilationRound.CURRENT, groovyFile)) {
          FSOperations.markDirty(context, CompilationRound.NEXT, groovyFile);
          FILES_MARKED_DIRTY_FOR_NEXT_ROUND.set(context, Boolean.TRUE);
        }
      } catch (IOException e) {
        LOG.error(e);
      }
    }
  }
}
  @NotNull
  private GroovycOutputParser runGroovycOrContinuation(
      CompileContext context,
      ModuleChunk chunk,
      JpsGroovySettings settings,
      Map<ModuleBuildTarget, String> finalOutputs,
      String compilerOutput,
      List<File> toCompile,
      boolean hasStubExcludes)
      throws Exception {
    GroovycContinuation continuation = takeContinuation(context, chunk);
    if (continuation != null) {
      if (Utils.IS_TEST_MODE || LOG.isDebugEnabled()) {
        LOG.info("using continuation for " + chunk);
      }
      return continuation.continueCompilation();
    }

    final Set<String> toCompilePaths = getPathsToCompile(toCompile);

    JpsSdk<JpsDummyElement> jdk = getJdk(chunk);
    String version = jdk == null ? SystemInfo.JAVA_RUNTIME_VERSION : jdk.getVersionString();
    boolean inProcess = "true".equals(System.getProperty("groovyc.in.process", "true"));
    boolean mayDependOnUtilJar =
        version != null && StringUtil.compareVersionNumbers(version, "1.6") >= 0;
    boolean optimizeClassLoading =
        !inProcess
            && mayDependOnUtilJar
            && ourOptimizeThreshold != 0
            && toCompilePaths.size() >= ourOptimizeThreshold;

    Map<String, String> class2Src =
        buildClassToSourceMap(chunk, context, toCompilePaths, finalOutputs);

    final String encoding =
        context
            .getProjectDescriptor()
            .getEncodingConfiguration()
            .getPreferredModuleChunkEncoding(chunk);
    List<String> patchers = new ArrayList<String>();

    for (GroovyBuilderExtension extension :
        JpsServiceManager.getInstance().getExtensions(GroovyBuilderExtension.class)) {
      patchers.addAll(extension.getCompilationUnitPatchers(context, chunk));
    }

    Collection<String> classpath = generateClasspath(context, chunk);
    if (LOG.isDebugEnabled()) {
      LOG.debug("Optimized class loading: " + optimizeClassLoading);
      LOG.debug("Groovyc classpath: " + classpath);
    }

    final File tempFile =
        GroovycOutputParser.fillFileWithGroovycParameters(
            compilerOutput,
            toCompilePaths,
            finalOutputs.values(),
            class2Src,
            encoding,
            patchers,
            optimizeClassLoading ? StringUtil.join(classpath, File.pathSeparator) : "");
    GroovycFlavor groovyc =
        inProcess
            ? new InProcessGroovyc(finalOutputs.values(), hasStubExcludes)
            : new ForkedGroovyc(optimizeClassLoading, chunk);

    GroovycOutputParser parser = new GroovycOutputParser(chunk, context);

    continuation = groovyc.runGroovyc(classpath, myForStubs, settings, tempFile, parser);
    setContinuation(context, chunk, continuation);
    return parser;
  }
/** @author Eugene Zhuravlev Date: 9/21/11 */
public class JavaBuilder extends ModuleLevelBuilder {
  private static final Logger LOG =
      Logger.getInstance("#org.jetbrains.jps.incremental.java.JavaBuilder");
  public static final String BUILDER_NAME = "java";
  private static final String JAVA_EXTENSION = ".java";
  public static final boolean USE_EMBEDDED_JAVAC =
      System.getProperty(GlobalOptions.USE_EXTERNAL_JAVAC_OPTION) == null;
  private static final Key<Integer> JAVA_COMPILER_VERSION_KEY =
      Key.create("_java_compiler_version_");
  private static final Key<Boolean> IS_ENABLED = Key.create("_java_compiler_enabled_");
  private static final Key<AtomicReference<String>> COMPILER_VERSION_INFO =
      Key.create("_java_compiler_version_info_");

  private static final Set<String> FILTERED_OPTIONS =
      new HashSet<String>(Arrays.<String>asList("-target"));
  private static final Set<String> FILTERED_SINGLE_OPTIONS =
      new HashSet<String>(
          Arrays.<String>asList(
              "-g",
              "-deprecation",
              "-nowarn",
              "-verbose",
              "-proc:none",
              "-proc:only",
              "-proceedOnError"));

  private static final FileFilter JAVA_SOURCES_FILTER =
      SystemInfo.isFileSystemCaseSensitive
          ? new FileFilter() {
            public boolean accept(File file) {
              return file.getPath().endsWith(JAVA_EXTENSION);
            }
          }
          : new FileFilter() {
            public boolean accept(File file) {
              return StringUtil.endsWithIgnoreCase(file.getPath(), JAVA_EXTENSION);
            }
          };

  private final Executor myTaskRunner;
  private static final List<ClassPostProcessor> ourClassProcessors =
      new ArrayList<ClassPostProcessor>();

  public static void registerClassPostProcessor(ClassPostProcessor processor) {
    ourClassProcessors.add(processor);
  }

  public JavaBuilder(Executor tasksExecutor) {
    super(BuilderCategory.TRANSLATOR);
    myTaskRunner = new SequentialTaskExecutor(tasksExecutor);
    // add here class processors in the sequence they should be executed
  }

  @NotNull
  public String getPresentableName() {
    return BUILDER_NAME;
  }

  @Override
  public void buildStarted(CompileContext context) {
    final JpsProject project = context.getProjectDescriptor().getProject();
    final JpsJavaCompilerConfiguration config =
        JpsJavaExtensionService.getInstance().getCompilerConfiguration(project);
    final String compilerId = config == null ? JavaCompilers.JAVAC_ID : config.getJavaCompilerId();
    if (LOG.isDebugEnabled()) {
      LOG.debug("Java compiler ID: " + compilerId);
    }
    final boolean isJavac =
        JavaCompilers.JAVAC_ID.equalsIgnoreCase(compilerId)
            || JavaCompilers.JAVAC_API_ID.equalsIgnoreCase(compilerId);
    final boolean isEclipse =
        JavaCompilers.ECLIPSE_ID.equalsIgnoreCase(compilerId)
            || JavaCompilers.ECLIPSE_EMBEDDED_ID.equalsIgnoreCase(compilerId);
    IS_ENABLED.set(context, isJavac || isEclipse);
    String messageText = null;
    if (isJavac) {
      messageText =
          "Using javac " + System.getProperty("java.version") + " to compile java sources";
    } else if (isEclipse) {
      messageText = "Using eclipse compiler to compile java sources";
    }
    COMPILER_VERSION_INFO.set(context, new AtomicReference<String>(messageText));
  }

  public ExitCode build(
      final CompileContext context,
      final ModuleChunk chunk,
      DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
      OutputConsumer outputConsumer)
      throws ProjectBuildException {
    if (!IS_ENABLED.get(context, Boolean.TRUE)) {
      return ExitCode.NOTHING_DONE;
    }
    try {
      final Map<File, ModuleBuildTarget> filesToCompile =
          new THashMap<File, ModuleBuildTarget>(FileUtil.FILE_HASHING_STRATEGY);

      dirtyFilesHolder.processDirtyFiles(
          new FileProcessor<JavaSourceRootDescriptor, ModuleBuildTarget>() {
            public boolean apply(
                ModuleBuildTarget target, File file, JavaSourceRootDescriptor descriptor)
                throws IOException {
              if (JAVA_SOURCES_FILTER.accept(file)) {
                filesToCompile.put(file, target);
              }
              return true;
            }
          });

      if (context.isMake()) {
        final ProjectBuilderLogger logger = context.getLoggingManager().getProjectBuilderLogger();
        if (logger.isEnabled()) {
          if (filesToCompile.size() > 0) {
            logger.logCompiledFiles(filesToCompile.keySet(), BUILDER_NAME, "Compiling files:");
          }
        }
      }

      return compile(context, chunk, dirtyFilesHolder, filesToCompile.keySet(), outputConsumer);
    } catch (ProjectBuildException e) {
      throw e;
    } catch (Exception e) {
      String message = e.getMessage();
      if (message == null) {
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        final PrintStream stream = new PrintStream(out);
        try {
          e.printStackTrace(stream);
        } finally {
          stream.close();
        }
        message = "Internal error: \n" + out.toString();
      }
      context.processMessage(new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, message));
      throw new ProjectBuildException(message, e);
    }
  }

  @Override
  public boolean shouldHonorFileEncodingForCompilation(File file) {
    return JAVA_SOURCES_FILTER.accept(file);
  }

  private ExitCode compile(
      final CompileContext context,
      ModuleChunk chunk,
      DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
      Collection<File> files,
      OutputConsumer outputConsumer)
      throws Exception {
    ExitCode exitCode = ExitCode.NOTHING_DONE;

    final boolean hasSourcesToCompile = !files.isEmpty();

    if (!hasSourcesToCompile && !dirtyFilesHolder.hasRemovedFiles()) {
      return exitCode;
    }

    final ProjectDescriptor pd = context.getProjectDescriptor();

    JavaBuilderUtil.ensureModuleHasJdk(
        chunk.representativeTarget().getModule(), context, BUILDER_NAME);
    final Collection<File> classpath =
        ProjectPaths.getCompilationClasspath(chunk, false /*context.isProjectRebuild()*/);
    final Collection<File> platformCp =
        ProjectPaths.getPlatformCompilationClasspath(chunk, false /*context.isProjectRebuild()*/);

    // begin compilation round
    final DiagnosticSink diagnosticSink = new DiagnosticSink(context);
    final Mappings delta = pd.dataManager.getMappings().createDelta();
    final Callbacks.Backend mappingsCallback = delta.getCallback();
    final OutputFilesSink outputSink =
        new OutputFilesSink(context, outputConsumer, mappingsCallback, chunk.getName());
    try {
      if (hasSourcesToCompile) {
        final AtomicReference<String> ref = COMPILER_VERSION_INFO.get(context);
        final String versionInfo =
            ref.getAndSet(null); // display compiler version info only once per compile session
        if (versionInfo != null) {
          LOG.info(versionInfo);
          context.processMessage(new CompilerMessage("", BuildMessage.Kind.INFO, versionInfo));
        }
        exitCode = ExitCode.OK;

        final Set<File> srcPath = new HashSet<File>();
        final BuildRootIndex index = pd.getBuildRootIndex();
        for (ModuleBuildTarget target : chunk.getTargets()) {
          for (JavaSourceRootDescriptor rd : index.getTempTargetRoots(target, context)) {
            srcPath.add(rd.root);
          }
        }

        final String chunkName = chunk.getName();
        context.processMessage(new ProgressMessage("Parsing java... [" + chunkName + "]"));

        final int filesCount = files.size();
        boolean compiledOk = true;
        if (filesCount > 0) {
          LOG.info(
              "Compiling "
                  + filesCount
                  + " java files; module: "
                  + chunkName
                  + (chunk.containsTests() ? " (tests)" : ""));
          if (LOG.isDebugEnabled()) {
            for (File file : files) {
              LOG.debug("Compiling " + file.getPath());
            }
            LOG.debug(" classpath for " + chunkName + ":");
            for (File file : classpath) {
              LOG.debug("  " + file.getAbsolutePath());
            }
            LOG.debug(" platform classpath for " + chunkName + ":");
            for (File file : platformCp) {
              LOG.debug("  " + file.getAbsolutePath());
            }
          }
          compiledOk =
              compileJava(
                  context,
                  chunk,
                  files,
                  classpath,
                  platformCp,
                  srcPath,
                  diagnosticSink,
                  outputSink);
        }

        context.checkCanceled();

        if (!compiledOk && diagnosticSink.getErrorCount() == 0) {
          diagnosticSink.report(
              new PlainMessageDiagnostic(
                  Diagnostic.Kind.ERROR, "Compilation failed: internal java compiler error"));
        }
        if (!Utils.PROCEED_ON_ERROR_KEY.get(context, Boolean.FALSE)
            && diagnosticSink.getErrorCount() > 0) {
          if (!compiledOk) {
            diagnosticSink.report(
                new PlainMessageDiagnostic(
                    Diagnostic.Kind.OTHER,
                    "Errors occurred while compiling module '" + chunkName + "'"));
          }
          throw new ProjectBuildException(
              "Compilation failed: errors: "
                  + diagnosticSink.getErrorCount()
                  + "; warnings: "
                  + diagnosticSink.getWarningCount());
        }
      }
    } finally {
      if (JavaBuilderUtil.updateMappings(
          context, delta, dirtyFilesHolder, chunk, files, outputSink.getSuccessfullyCompiled())) {
        exitCode = ExitCode.ADDITIONAL_PASS_REQUIRED;
      }
    }

    return exitCode;
  }

  private boolean compileJava(
      final CompileContext context,
      ModuleChunk chunk,
      Collection<File> files,
      Collection<File> classpath,
      Collection<File> platformCp,
      Collection<File> sourcePath,
      DiagnosticOutputConsumer diagnosticSink,
      final OutputFileConsumer outputSink)
      throws Exception {

    final TasksCounter counter = new TasksCounter();
    COUNTER_KEY.set(context, counter);

    final JpsJavaExtensionService javaExt = JpsJavaExtensionService.getInstance();
    final JpsJavaCompilerConfiguration compilerConfig =
        javaExt.getCompilerConfiguration(context.getProjectDescriptor().getProject());
    assert compilerConfig != null;

    final Set<JpsModule> modules = chunk.getModules();
    ProcessorConfigProfile profile = null;
    if (modules.size() == 1) {
      final JpsModule module = modules.iterator().next();
      profile = compilerConfig.getAnnotationProcessingProfile(module);
    } else {
      // perform cycle-related validations
      Pair<String, LanguageLevel> pair = null;
      for (JpsModule module : modules) {
        final LanguageLevel moduleLevel = javaExt.getLanguageLevel(module);
        if (pair == null) {
          pair = Pair.create(module.getName(), moduleLevel); // first value
        } else {
          if (!Comparing.equal(pair.getSecond(), moduleLevel)) {
            final String message =
                "Modules "
                    + pair.getFirst()
                    + " and "
                    + module.getName()
                    + " must have the same language level because of cyclic dependencies between them";
            diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, message));
            return true;
          }
        }
      }

      // check that all chunk modules are excluded from annotation processing
      for (JpsModule module : modules) {
        final ProcessorConfigProfile prof = compilerConfig.getAnnotationProcessingProfile(module);
        if (prof.isEnabled()) {
          final String message =
              "Annotation processing is not supported for module cycles. Please ensure that all modules from cycle ["
                  + chunk.getName()
                  + "] are excluded from annotation processing";
          diagnosticSink.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, message));
          return true;
        }
      }
    }

    final Map<File, Set<File>> outs = buildOutputDirectoriesMap(context, chunk);
    final List<String> options = getCompilationOptions(context, chunk, profile);
    final ClassProcessingConsumer classesConsumer =
        new ClassProcessingConsumer(context, outputSink);
    if (LOG.isDebugEnabled()) {
      LOG.debug(
          "Compiling chunk ["
              + chunk.getName()
              + "] with options: \""
              + StringUtil.join(options, " ")
              + "\"");
    }
    try {
      final boolean rc;
      if (USE_EMBEDDED_JAVAC) {
        final boolean useEclipse = useEclipseCompiler(context);
        rc =
            JavacMain.compile(
                options,
                files,
                classpath,
                platformCp,
                sourcePath,
                outs,
                diagnosticSink,
                classesConsumer,
                context.getCancelStatus(),
                useEclipse);
      } else {
        final JavacServerClient client = ensureJavacServerLaunched(context);
        final RequestFuture<JavacServerResponseHandler> future =
            client.sendCompileRequest(
                options,
                files,
                classpath,
                platformCp,
                sourcePath,
                outs,
                diagnosticSink,
                classesConsumer);
        while (!future.waitFor(100L, TimeUnit.MILLISECONDS)) {
          if (context.getCancelStatus().isCanceled()) {
            future.cancel(false);
          }
        }
        rc = future.getMessageHandler().isTerminatedSuccessfully();
      }
      return rc;
    } finally {
      counter.await();
    }
  }

  private static boolean useEclipseCompiler(CompileContext context) {
    if (!USE_EMBEDDED_JAVAC) {
      return false;
    }
    JpsProject project = context.getProjectDescriptor().getProject();
    final JpsJavaCompilerConfiguration configuration =
        JpsJavaExtensionService.getInstance().getCompilerConfiguration(project);
    final String compilerId = configuration != null ? configuration.getJavaCompilerId() : null;
    return JavaCompilers.ECLIPSE_ID.equalsIgnoreCase(compilerId)
        || JavaCompilers.ECLIPSE_EMBEDDED_ID.equalsIgnoreCase(compilerId);
  }

  private void submitAsyncTask(CompileContext context, final Runnable taskRunnable) {
    final TasksCounter counter = COUNTER_KEY.get(context);

    assert counter != null;

    counter.incTaskCount();
    myTaskRunner.execute(
        new Runnable() {
          public void run() {
            try {
              taskRunnable.run();
            } finally {
              counter.decTaskCounter();
            }
          }
        });
  }

  private static synchronized JavacServerClient ensureJavacServerLaunched(CompileContext context)
      throws Exception {
    final ExternalJavacDescriptor descriptor = ExternalJavacDescriptor.KEY.get(context);
    if (descriptor != null) {
      return descriptor.client;
    }
    // start server here
    final int port = findFreePort();
    final int heapSize = getJavacServerHeapSize(context);

    // defaulting to the same jdk that used to run the build process
    String javaHome = SystemProperties.getJavaHome();
    int javaVersion = convertToNumber(SystemProperties.getJavaVersion());

    for (JpsSdk<?> sdk : context.getProjectDescriptor().getProjectJavaSdks()) {
      final String version = sdk.getVersionString();
      final int ver = convertToNumber(version);
      if (ver > javaVersion) {
        javaVersion = ver;
        javaHome = sdk.getHomePath();
      }
    }

    final BaseOSProcessHandler processHandler =
        JavacServerBootstrap.launchJavacServer(
            javaHome, heapSize, port, Utils.getSystemRoot(), getCompilationVMOptions(context));
    final JavacServerClient client = new JavacServerClient();
    try {
      client.connect("127.0.0.1", port);
    } catch (Throwable ex) {
      processHandler.destroyProcess();
      throw new Exception("Failed to connect to external javac process: ", ex);
    }
    ExternalJavacDescriptor.KEY.set(context, new ExternalJavacDescriptor(processHandler, client));
    return client;
  }

  private static int convertToNumber(String ver) {
    if (ver == null) {
      return 0;
    }
    final int quoteBegin = ver.indexOf("\"");
    if (quoteBegin >= 0) {
      final int quoteEnd = ver.indexOf("\"", quoteBegin + 1);
      if (quoteEnd > quoteBegin) {
        ver = ver.substring(quoteBegin + 1, quoteEnd);
      }
    }
    if (ver.isEmpty()) {
      return 0;
    }

    final String prefix = "1.";
    final int parseBegin = ver.startsWith(prefix) ? prefix.length() : 0;

    final int parseEnd = ver.indexOf(".", parseBegin);
    if (parseEnd > 0) {
      ver = ver.substring(parseBegin, parseEnd);
    } else {
      ver = ver.substring(parseBegin);
    }

    try {
      return Integer.parseInt(ver);
    } catch (NumberFormatException ignored) {
    }
    return 0;
  }

  private static int findFreePort() {
    try {
      final ServerSocket serverSocket = new ServerSocket(0);
      try {
        return serverSocket.getLocalPort();
      } finally {
        // workaround for linux : calling close() immediately after opening socket
        // may result that socket is not closed
        synchronized (serverSocket) {
          try {
            serverSocket.wait(1);
          } catch (Throwable ignored) {
          }
        }
        serverSocket.close();
      }
    } catch (IOException e) {
      e.printStackTrace(System.err);
      return JavacServer.DEFAULT_SERVER_PORT;
    }
  }

  private static int getJavacServerHeapSize(CompileContext context) {
    final JpsProject project = context.getProjectDescriptor().getProject();
    final JpsJavaCompilerConfiguration config =
        JpsJavaExtensionService.getInstance().getOrCreateCompilerConfiguration(project);
    final JpsJavaCompilerOptions options = config.getCurrentCompilerOptions();
    return options.MAXIMUM_HEAP_SIZE;
  }

  private static final Key<List<String>> JAVAC_OPTIONS = Key.create("_javac_options_");
  private static final Key<List<String>> JAVAC_VM_OPTIONS = Key.create("_javac_vm_options_");

  private static List<String> getCompilationVMOptions(CompileContext context) {
    List<String> cached = JAVAC_VM_OPTIONS.get(context);
    if (cached == null) {
      loadCommonJavacOptions(context);
      cached = JAVAC_VM_OPTIONS.get(context);
    }
    return cached;
  }

  private static List<String> getCompilationOptions(
      CompileContext context, ModuleChunk chunk, @Nullable ProcessorConfigProfile profile) {
    List<String> cached = JAVAC_OPTIONS.get(context);
    if (cached == null) {
      loadCommonJavacOptions(context);
      cached = JAVAC_OPTIONS.get(context);
      assert cached != null : context;
    }

    List<String> options = new ArrayList<String>(cached);
    addCompilationOptions(options, context, chunk, profile);
    return options;
  }

  public static void addCompilationOptions(
      List<String> options,
      CompileContext context,
      ModuleChunk chunk,
      @Nullable ProcessorConfigProfile profile) {
    if (!isEncodingSet(options)) {
      final CompilerEncodingConfiguration config =
          context.getProjectDescriptor().getEncodingConfiguration();
      final String encoding = config.getPreferredModuleChunkEncoding(chunk);
      if (config.getAllModuleChunkEncodings(chunk).size() > 1) {
        final StringBuilder msgBuilder = new StringBuilder();
        msgBuilder.append("Multiple encodings set for module chunk ").append(chunk.getName());
        if (encoding != null) {
          msgBuilder.append("\n\"").append(encoding).append("\" will be used by compiler");
        }
        context.processMessage(
            new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.INFO, msgBuilder.toString()));
      }
      if (!StringUtil.isEmpty(encoding)) {
        options.add("-encoding");
        options.add(encoding);
      }
    }

    final String langLevel = getLanguageLevel(chunk.getModules().iterator().next());
    if (!StringUtil.isEmpty(langLevel)) {
      options.add("-source");
      options.add(langLevel);
    }

    JpsJavaCompilerConfiguration compilerConfiguration =
        JpsJavaExtensionService.getInstance()
            .getOrCreateCompilerConfiguration(context.getProjectDescriptor().getProject());
    String bytecodeTarget = null;
    int chunkSdkVersion = -1;
    for (JpsModule module : chunk.getModules()) {
      final JpsSdk<JpsDummyElement> sdk = module.getSdk(JpsJavaSdkType.INSTANCE);
      if (sdk != null) {
        final int moduleSdkVersion = convertToNumber(sdk.getVersionString());
        if (moduleSdkVersion != 0 /*could determine the version*/
            && (chunkSdkVersion < 0 || chunkSdkVersion > moduleSdkVersion)) {
          chunkSdkVersion = moduleSdkVersion;
        }
      }

      final String moduleTarget = compilerConfiguration.getByteCodeTargetLevel(module.getName());
      if (moduleTarget == null) {
        continue;
      }
      if (bytecodeTarget == null) {
        bytecodeTarget = moduleTarget;
      } else {
        if (moduleTarget.compareTo(bytecodeTarget) < 0) {
          bytecodeTarget =
              moduleTarget; // use the lower possible target among modules that form the chunk
        }
      }
    }
    if (bytecodeTarget != null) {
      options.add("-target");
      options.add(bytecodeTarget);
    } else {
      if (chunkSdkVersion > 0 && getCompilerSdkVersion(context) > chunkSdkVersion) {
        // force lower bytecode target level to match the version of sdk assigned to this chunk
        options.add("-target");
        options.add("1." + chunkSdkVersion);
      }
    }

    if (profile != null && profile.isEnabled()) {
      // configuring annotation processing
      if (!profile.isObtainProcessorsFromClasspath()) {
        final String processorsPath = profile.getProcessorPath();
        options.add("-processorpath");
        options.add(
            processorsPath == null ? "" : FileUtil.toSystemDependentName(processorsPath.trim()));
      }

      final Set<String> processors = profile.getProcessors();
      if (!processors.isEmpty()) {
        options.add("-processor");
        options.add(StringUtil.join(processors, ","));
      }

      for (Map.Entry<String, String> optionEntry : profile.getProcessorOptions().entrySet()) {
        options.add("-A" + optionEntry.getKey() + "=" + optionEntry.getValue());
      }

      final File srcOutput =
          ProjectPaths.getAnnotationProcessorGeneratedSourcesOutputDir(
              chunk.getModules().iterator().next(), chunk.containsTests(), profile);
      if (srcOutput != null) {
        srcOutput.mkdirs();
        options.add("-s");
        options.add(srcOutput.getPath());
      }
    } else {
      options.add("-proc:none");
    }
  }

  private static String getLanguageLevel(JpsModule module) {
    LanguageLevel level = JpsJavaExtensionService.getInstance().getLanguageLevel(module);
    if (level == null) return null;
    switch (level) {
      case JDK_1_3:
        return "1.3";
      case JDK_1_4:
        return "1.4";
      case JDK_1_5:
        return "1.5";
      case JDK_1_6:
        return "1.6";
      case JDK_1_7:
        return "1.7";
      case JDK_1_8:
        return "8";
      default:
        return null;
    }
  }

  private static boolean isEncodingSet(List<String> options) {
    for (String option : options) {
      if ("-encoding".equals(option)) {
        return true;
      }
    }
    return false;
  }

  private static int getCompilerSdkVersion(CompileContext context) {
    final Integer cached = JAVA_COMPILER_VERSION_KEY.get(context);
    if (cached != null) {
      return cached;
    }
    int javaVersion = convertToNumber(SystemProperties.getJavaVersion());
    if (!USE_EMBEDDED_JAVAC) {
      // in case of external javac, run compiler from the newest jdk that is used in the project
      for (JpsSdk<?> sdk : context.getProjectDescriptor().getProjectJavaSdks()) {
        final String version = sdk.getVersionString();
        final int ver = convertToNumber(version);
        if (ver > javaVersion) {
          javaVersion = ver;
        }
      }
    }
    JAVA_COMPILER_VERSION_KEY.set(context, javaVersion);
    return javaVersion;
  }

  private static void loadCommonJavacOptions(CompileContext context) {
    final List<String> options = new ArrayList<String>();
    final List<String> vmOptions = new ArrayList<String>();

    final JpsProject project = context.getProjectDescriptor().getProject();
    final JpsJavaCompilerConfiguration compilerConfig =
        JpsJavaExtensionService.getInstance().getOrCreateCompilerConfiguration(project);
    final JpsJavaCompilerOptions compilerOptions = compilerConfig.getCurrentCompilerOptions();
    if (compilerOptions.DEBUGGING_INFO) {
      options.add("-g");
    }
    if (compilerOptions.DEPRECATION) {
      options.add("-deprecation");
    }
    if (compilerOptions.GENERATE_NO_WARNINGS) {
      options.add("-nowarn");
    }
    if (compilerOptions instanceof EclipseCompilerOptions) {
      final EclipseCompilerOptions eclipseOptions = (EclipseCompilerOptions) compilerOptions;
      if (eclipseOptions.PROCEED_ON_ERROR) {
        options.add("-proceedOnError");
      }
    }
    final String customArgs = compilerOptions.ADDITIONAL_OPTIONS_STRING;
    if (customArgs != null) {
      final StringTokenizer customOptsTokenizer = new StringTokenizer(customArgs, " \t\r\n");
      boolean skip = false;
      while (customOptsTokenizer.hasMoreTokens()) {
        final String userOption = customOptsTokenizer.nextToken();
        if (FILTERED_OPTIONS.contains(userOption)) {
          skip = true;
          continue;
        }
        if (!skip) {
          if (!FILTERED_SINGLE_OPTIONS.contains(userOption)) {
            if (userOption.startsWith("-J-")) {
              vmOptions.add(userOption.substring("-J".length()));
            } else {
              options.add(userOption);
            }
          }
        }
      }
    }

    if (useEclipseCompiler(context)) {
      for (String option : options) {
        if (option.startsWith("-proceedOnError")) {
          Utils.PROCEED_ON_ERROR_KEY.set(context, Boolean.TRUE);
          break;
        }
      }
    }

    JAVAC_OPTIONS.set(context, options);
    JAVAC_VM_OPTIONS.set(context, vmOptions);
  }

  @Override
  public void chunkBuildFinished(CompileContext context, ModuleChunk chunk) {
    JavaBuilderUtil.cleanupChunkResources(context);
  }

  private static Map<File, Set<File>> buildOutputDirectoriesMap(
      CompileContext context, ModuleChunk chunk) {
    final Map<File, Set<File>> map = new THashMap<File, Set<File>>(FileUtil.FILE_HASHING_STRATEGY);
    for (ModuleBuildTarget target : chunk.getTargets()) {
      final File outputDir = target.getOutputDir();
      if (outputDir == null) {
        continue;
      }
      final Set<File> roots = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY);
      for (JavaSourceRootDescriptor descriptor :
          context.getProjectDescriptor().getBuildRootIndex().getTargetRoots(target, context)) {
        roots.add(descriptor.root);
      }
      map.put(outputDir, roots);
    }
    return map;
  }

  private class DiagnosticSink implements DiagnosticOutputConsumer {
    private final CompileContext myContext;
    private volatile int myErrorCount = 0;
    private volatile int myWarningCount = 0;

    public DiagnosticSink(CompileContext context) {
      myContext = context;
    }

    public void registerImports(
        final String className,
        final Collection<String> imports,
        final Collection<String> staticImports) {
      // submitAsyncTask(myContext, new Runnable() {
      //  public void run() {
      //    final Callbacks.Backend callback = DELTA_MAPPINGS_CALLBACK_KEY.get(myContext);
      //    if (callback != null) {
      //      callback.registerImports(className, imports, staticImports);
      //    }
      //  }
      // });
    }

    public void outputLineAvailable(String line) {
      if (!StringUtil.isEmpty(line)) {
        if (line.contains("java.lang.OutOfMemoryError")) {
          myContext.processMessage(
              new CompilerMessage(
                  BUILDER_NAME, BuildMessage.Kind.ERROR, "OutOfMemoryError: insufficient memory"));
          myErrorCount++;
        } else {
          final BuildMessage.Kind kind = getKindByMessageText(line);
          if (kind == BuildMessage.Kind.ERROR) {
            myErrorCount++;
          } else if (kind == BuildMessage.Kind.WARNING) {
            myWarningCount++;
          }
          myContext.processMessage(new CompilerMessage(BUILDER_NAME, kind, line));
        }
      }
    }

    private BuildMessage.Kind getKindByMessageText(String line) {
      final String lowercasedLine = line.toLowerCase(Locale.US);
      if (lowercasedLine.contains("error") || lowercasedLine.contains("requires target release")) {
        return BuildMessage.Kind.ERROR;
      }
      return BuildMessage.Kind.INFO;
    }

    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:
        case NOTE:
          kind = BuildMessage.Kind.WARNING;
          myWarningCount++;
          break;
        default:
          kind = BuildMessage.Kind.INFO;
      }
      File sourceFile = null;
      try {
        // for eclipse compiler just an attempt to call getSource() may lead to an NPE,
        // so calling this method under try/catch to avoid induced compiler errors
        final JavaFileObject source = diagnostic.getSource();
        sourceFile = source != null ? Utils.convertToFile(source.toUri()) : null;
      } catch (Exception e) {
        LOG.info(e);
      }
      final String srcPath =
          sourceFile != null ? FileUtil.toSystemIndependentName(sourceFile.getPath()) : null;
      String message = diagnostic.getMessage(Locale.US);
      if (Utils.IS_TEST_MODE) {
        LOG.info(message);
      }
      myContext.processMessage(
          new CompilerMessage(
              BUILDER_NAME,
              kind,
              message,
              srcPath,
              diagnostic.getStartPosition(),
              diagnostic.getEndPosition(),
              diagnostic.getPosition(),
              diagnostic.getLineNumber(),
              diagnostic.getColumnNumber()));
    }

    public int getErrorCount() {
      return myErrorCount;
    }

    public int getWarningCount() {
      return myWarningCount;
    }
  }

  private class ClassProcessingConsumer implements OutputFileConsumer {
    private final CompileContext myContext;
    private final OutputFileConsumer myDelegateOutputFileSink;

    public ClassProcessingConsumer(CompileContext context, OutputFileConsumer sink) {
      myContext = context;
      myDelegateOutputFileSink =
          sink != null
              ? sink
              : new OutputFileConsumer() {
                public void save(@NotNull OutputFileObject fileObject) {
                  throw new RuntimeException("Output sink for compiler was not specified");
                }
              };
    }

    public void save(@NotNull final OutputFileObject fileObject) {
      if (JavaFileObject.Kind.CLASS != fileObject.getKind()) {
        // generated sources or resources must be saved synchronously, because some compilers (e.g.
        // eclipse)
        // may want to read generated text for further compilation
        try {
          final BinaryContent content = fileObject.getContent();
          if (content != null) {
            content.saveToFile(fileObject.getFile());
          }
        } catch (IOException e) {
          myContext.processMessage(
              new CompilerMessage(BUILDER_NAME, BuildMessage.Kind.ERROR, e.getMessage()));
        }
      }

      submitAsyncTask(
          myContext,
          new Runnable() {
            public void run() {
              try {
                for (ClassPostProcessor processor : ourClassProcessors) {
                  processor.process(myContext, fileObject);
                }
              } finally {
                myDelegateOutputFileSink.save(fileObject);
              }
            }
          });
    }
  }

  private static final Key<TasksCounter> COUNTER_KEY = Key.create("_async_task_counter_");

  private static final class TasksCounter {
    private int myCounter = 0;

    public synchronized void incTaskCount() {
      myCounter++;
    }

    public synchronized void decTaskCounter() {
      myCounter = Math.max(0, myCounter - 1);
      if (myCounter == 0) {
        notifyAll();
      }
    }

    public synchronized void await() {
      while (myCounter > 0) {
        try {
          wait();
        } catch (InterruptedException e) {
        }
      }
    }
  }
}