package com.intellij.javascript.flex.maven;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.configurations.ParametersList;
import com.intellij.execution.configurations.SimpleJavaParameters;
import com.intellij.flex.FlexCommonUtils;
import com.intellij.lang.javascript.flex.FlexBundle;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProviderImpl;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.SimpleJavaSdkType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ArrayUtil;
import com.intellij.util.PathsList;
import com.intellij.util.StringBuilderSpinAllocator;
import com.intellij.util.SystemProperties;
import gnu.trove.THashMap;
import gnu.trove.TObjectObjectProcedure;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.idea.maven.execution.MavenExternalParameters;
import org.jetbrains.idea.maven.importing.MavenRootModelAdapter;
import org.jetbrains.idea.maven.model.MavenId;
import org.jetbrains.idea.maven.project.*;
import org.jetbrains.idea.maven.utils.MavenProcessCanceledException;
import org.jetbrains.idea.maven.utils.MavenProgressIndicator;
import org.jetbrains.idea.maven.utils.MavenUtil;
import org.jetbrains.jps.model.java.JavaSourceRootType;
import javax.swing.event.HyperlinkEvent;
import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.jetbrains.idea.maven.utils.MavenLog.LOG;
class Flexmojos4GenerateConfigTask extends MavenProjectsProcessorBasicTask {
private static final Pattern RESULT_PATTERN =
Pattern.compile("^\\[fcg\\] generated: (\\d+):([^|]+)\\|(.+)\\[/fcg\\]$", Pattern.MULTILINE);
private static final String ERROR = "[ERROR]";
private DataOutputStream out;
private Process process;
private MavenProgressIndicator indicator;
private final List<MavenProject> projects = new ArrayList<>();
private final Map<Module, String> myModuleToConfigFilePath = new THashMap<>();
private RefreshConfigFiles postTask;
public Flexmojos4GenerateConfigTask(MavenProjectsTree tree) {
//noinspection NullableProblems
super(null, tree);
}
private static String getSettingsFilePath(File settingsFile) {
return settingsFile == null || !settingsFile.exists() ? " " : settingsFile.getAbsolutePath();
}
@Override
public void perform(Project project, MavenEmbeddersManager embeddersManager, MavenConsole console, final MavenProgressIndicator indicator)
throws MavenProcessCanceledException {
final long start = System.currentTimeMillis();
this.indicator = indicator;
indicator.setText(FlexBundle.message("generating.flex.configs"));
try {
runGeneratorServer(MavenProjectsManager.getInstance(project), project);
writeProjects(project);
}
catch (IOException e) {
showWarning(project, e.getMessage());
LOG.error(e);
}
catch (ExecutionException e) {
showWarning(project, e.getMessage());
}
if (process == null) {
return;
}
//noinspection WhileLoopSpinsOnField
while (process != null) {
try {
//noinspection BusyWait
Thread.sleep(500);
}
catch (InterruptedException ignored) {
break;
}
if (indicator.isCanceled()) {
LOG.warn("Generating flex configs canceled");
if (process != null) {
process.destroy();
}
break;
}
}
if (postTask != null) {
MavenUtil.invokeAndWait(project, postTask);
MavenUtil.invokeAndWaitWriteAction(project, () -> {
for (Map.Entry<Module, String> entry : myModuleToConfigFilePath.entrySet()) {
if (entry.getKey().isDisposed()) continue;
final VirtualFile configFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(entry.getValue());
if (configFile != null && !configFile.isDirectory()) {
Flexmojos3GenerateConfigTask.updateMainClass(entry.getKey(), configFile);
}
}
});
}
final long duration = System.currentTimeMillis() - start;
LOG.info("Generating flex configs took " + duration + " ms: " + duration / 60000 + " min " + (duration % 60000) / 1000 + "sec");
}
private void writeProjects(Project project) throws IOException {
assert !projects.isEmpty();
out.writeShort(projects.size());
MavenProject outdatedIdeaMavenPluginHolder = null;
for (MavenProject pendingProject : projects) {
if (outdatedIdeaMavenPluginHolder == null &&
pendingProject.findPlugin("com.intellij.flex.maven", "idea-flexmojos-maven-plugin") != null) {
outdatedIdeaMavenPluginHolder = pendingProject;
}
out.writeUTF(pendingProject.getFile().getPath());
}
out.flush();
if (outdatedIdeaMavenPluginHolder != null) {
new Notification("Maven", FlexBundle.message("flexmojos.project.import"), FlexBundle.message(
"flexmojos.maven.plugin.outdated.warning", outdatedIdeaMavenPluginHolder.getMavenId().toString()), NotificationType.WARNING)
.notify(project);
}
}
void submit(MavenProject mavenProject, final Module module, final String configFilePath) {
assert out == null;
assert !projects.contains(mavenProject): mavenProject.getName();
projects.add(mavenProject);
myModuleToConfigFilePath.put(module, configFilePath);
}
private void runGeneratorServer(MavenProjectsManager mavenProjectsManager, Project project)
throws IOException, ExecutionException, MavenProcessCanceledException {
final SimpleJavaParameters params = new SimpleJavaParameters();
params.setJdk(new SimpleJavaSdkType().createJdk("tmp", SystemProperties.getJavaHome()));
final MavenGeneralSettings mavenGeneralSettings = mavenProjectsManager.getGeneralSettings();
final ParametersList programParametersList = params.getProgramParametersList();
programParametersList.add(getSettingsFilePath(mavenGeneralSettings.getEffectiveGlobalSettingsIoFile()));
programParametersList.add(getSettingsFilePath(mavenGeneralSettings.getEffectiveUserSettingsIoFile()));
programParametersList.add(mavenGeneralSettings.getEffectiveLocalRepository().getAbsolutePath());
programParametersList.add(mavenGeneralSettings.isWorkOffline() ? "t" : "f");
//noinspection ConstantConditions
programParametersList.add(project.getBaseDir().getPath() + "/.idea/flexmojos");
configureMavenClassPath(mavenGeneralSettings, params.getClassPath());
final File userVmP = new File(SystemProperties.getUserHome(), "fcg-vmp");
if (userVmP.exists()) {
params.getVMParametersList().addParametersString(FileUtil.loadFile(userVmP));
}
params.setMainClass("com.intellij.flex.maven.GeneratorServer");
final GeneralCommandLine commandLine = params.toCommandLine();
commandLine.setRedirectErrorStream(true);
LOG.info("Generate Flex Configs Task:" + commandLine.getCommandLineString());
indicator.checkCanceled();
process = commandLine.createProcess();
ApplicationManager.getApplication().executeOnPooledThread(new OutputReader(project));
//noinspection IOResourceOpenedButNotSafelyClosed
out = new DataOutputStream(new BufferedOutputStream(process.getOutputStream()));
writeExplicitProfiles(mavenProjectsManager.getExplicitProfiles().getEnabledProfiles());
writeWorkspaceMap(myTree.getProjects());
out.writeUTF(FlexCommonUtils.getPathToBundledJar("flexmojos-idea-configurator.jar"));
out.writeUTF(getIdeaConfiguratorClassName());
}
protected String getIdeaConfiguratorClassName() {
return "com.intellij.flex.maven.IdeaConfigurator";
}
private final class OutputReader implements Runnable {
private final Project project;
public OutputReader(Project project) {
this.project = project;
}
@Override
public void run() {
final StringBuilder stringBuilder = StringBuilderSpinAllocator.alloc();
int exitCode = -1;
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
final InputStreamReader reader = new InputStreamReader(process.getInputStream());
final List<String> filesForRefresh = new ArrayList<>(projects.size());
final THashMap<MavenProject, List<String>> sourceRoots = new THashMap<>(projects.size());
try {
char[] buf = new char[8196];
int read;
final Matcher matcher = RESULT_PATTERN.matcher(stringBuilder);
int startForResultParse = 0;
while ((read = reader.read(buf, 0, buf.length)) >= 0) {
stringBuilder.append(buf, 0, read);
if (indicator.isCanceled()) {
process.destroy();
}
while (matcher.find(startForResultParse)) {
MavenProject mavenProject = projects.get(Integer.parseInt(matcher.group(1)));
indicator.setText2(mavenProject.getDisplayName());
filesForRefresh.add(matcher.group(2));
StringTokenizer tokenizer = new StringTokenizer(matcher.group(3), "|");
List<String> moduleSourcesRoots = new ArrayList<>();
while (tokenizer.hasMoreTokens()) {
moduleSourcesRoots.add(tokenizer.nextToken());
}
sourceRoots.put(mavenProject, moduleSourcesRoots);
startForResultParse = matcher.end();
}
}
try {
process.waitFor();
}
catch (InterruptedException ignored) {
}
exitCode = process.exitValue();
}
catch (IOException e) {
LOG.warn(stringBuilder.toString(), e);
}
finally {
process.destroy();
process = null;
final String result = stringBuilder.toString().replace('\r', '\n');
StringBuilderSpinAllocator.dispose(stringBuilder);
if (exitCode != 0) {
LOG.warn("Generating flex configs exited with exit code " + exitCode);
showWarning(project, "exit code: " + exitCode);
}
LOG.info("Generating flex configs out:\n" + result);
if (result.startsWith(ERROR) || result.contains("\n" + ERROR)) {
final StringBuilder errorBuf = new StringBuilder();
final List<String> lines = StringUtil.split(result, "\n");
for (String line : lines) {
if (line.startsWith(ERROR)) {
if (errorBuf.length() > 0) errorBuf.append("\n");
errorBuf.append(line);
}
}
showWarningWithDetails(project, errorBuf.toString());
}
if (!filesForRefresh.isEmpty()) {
postTask = new RefreshConfigFiles(filesForRefresh, sourceRoots, project);
}
}
}
}
private final static class RefreshConfigFiles implements Runnable {
private final List<String> filesForRefresh;
private final THashMap<MavenProject, List<String>> sourceRoots;
private final Project project;
public RefreshConfigFiles(List<String> filesForRefresh, THashMap<MavenProject, List<String>> sourceRoots, Project project) {
this.filesForRefresh = filesForRefresh;
this.sourceRoots = sourceRoots;
this.project = project;
}
@Override
public void run() {
WriteAction.run(() -> {
// need to refresh externally created file
final VirtualFile p = LocalFileSystem.getInstance().refreshAndFindFileByPath(Flexmojos4Configurator.getCompilerConfigsDir(project));
if (p == null) {
return;
}
p.refresh(false, true);
final List<VirtualFile> virtualFiles = new ArrayList<>(filesForRefresh.size());
for (String path : filesForRefresh) {
final VirtualFile file = p.findChild(path);
if (file != null) {
virtualFiles.add(file);
}
}
LocalFileSystem.getInstance().refreshFiles(virtualFiles);
final MavenProjectsManager mavenProjectsManager = MavenProjectsManager.getInstance(project);
sourceRoots.forEachEntry(new TObjectObjectProcedure<MavenProject, List<String>>() {
@Override
public boolean execute(MavenProject mavenProject, List<String> sourceRoots) {
final Module module = mavenProjectsManager.findModule(mavenProject);
if (module == null) return true;
IdeModifiableModelsProviderImpl provider = new IdeModifiableModelsProviderImpl(project);
MavenRootModelAdapter a = new MavenRootModelAdapter(mavenProject, module, provider);
for (String sourceRoot : sourceRoots) {
a.addSourceFolder(sourceRoot, JavaSourceRootType.SOURCE);
}
provider.commit();
return true;
}
});
});
}
}
private void writeExplicitProfiles(Collection<String> explicitProfiles) throws IOException {
out.writeShort(explicitProfiles.size());
if (explicitProfiles.isEmpty()) {
return;
}
for (String explicitProfile : explicitProfiles) {
out.writeUTF(explicitProfile);
}
}
private void writeWorkspaceMap(final Collection<MavenProject> mavenProjects) throws IOException {
int actualLength = 0;
for (MavenProject mavenProject : mavenProjects) {
if (ArrayUtil.contains(mavenProject.getPackaging(), FlexmojosImporter.SUPPORTED_PACKAGINGS)) {
actualLength++;
}
}
out.writeShort(actualLength);
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);
for (MavenProject mavenProject : mavenProjects) {
if (!ArrayUtil.contains(mavenProject.getPackaging(), FlexmojosImporter.SUPPORTED_PACKAGINGS)) {
continue;
}
final MavenId mavenId = mavenProject.getMavenId();
objectOutputStream.writeObject(mavenId.getGroupId());
objectOutputStream.writeObject(mavenId.getArtifactId());
objectOutputStream.writeObject(mavenId.getVersion());
objectOutputStream.writeObject(mavenProject.getFile().getPath());
}
objectOutputStream.flush();
}
private static void configureMavenClassPath(MavenGeneralSettings mavenGeneralSettings, PathsList classPath) throws ExecutionException {
String mavenHome = MavenExternalParameters.resolveMavenHome(mavenGeneralSettings);
String version = MavenUtil.getMavenVersion(mavenHome);
String pathToBundledJar = FlexCommonUtils.getPathToBundledJar(StringUtil.compareVersionNumbers(version, "3.1") >= 0
? "flexmojos-flex-configs-generator-server-31.jar"
: "flexmojos-flex-configs-generator-server.jar");
LOG.info("Generating flex configs pathToBundledJar: " + pathToBundledJar);
LOG.assertTrue(!StringUtil.isEmpty(pathToBundledJar));
classPath.add(pathToBundledJar);
final String libDirPath = mavenHome + "/lib";
for (String s : new File(libDirPath).list()) {
if (s.endsWith(".jar") && !s.startsWith("maven-embedder-") && !s.startsWith("commons-cli-") && !s.startsWith("nekohtml-")) {
classPath.add(libDirPath + File.separator + s);
}
}
// plexus-classworlds
final String libBootDirPath = mavenHome + "/boot";
for (String s : new File(libBootDirPath).list()) {
if (s.endsWith(".jar")) {
classPath.add(libBootDirPath + File.separator + s);
}
}
}
private static void showWarning(final Project project, final String text) {
new Notification("Maven", FlexBundle.message("flexmojos.project.import"), FlexBundle.message("flexmojos4.warning", text),
NotificationType.WARNING).notify(project);
}
private static void showWarningWithDetails(final Project project, final String details) {
final NotificationListener listener = new NotificationListener.Adapter() {
@Override
protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent e) {
Messages.showErrorDialog(project, FlexBundle.message("flexmojos4.details.start", details),
FlexBundle.message("flexmojos.project.import"));
notification.expire();
}
};
new Notification("Maven", FlexBundle.message("flexmojos.project.import"), FlexBundle.message("flexmojos4.warning.with.link"),
NotificationType.WARNING, listener).notify(project);
}
}