package org.elixir_lang.mix.importWizard;
import com.intellij.compiler.CompilerWorkspaceConfiguration;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.ModifiableModuleModel;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkTypeId;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
import com.intellij.openapi.roots.ui.configuration.ModulesProvider;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileVisitor;
import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl;
import com.intellij.packaging.artifacts.ModifiableArtifactModel;
import com.intellij.projectImport.ProjectImportBuilder;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import org.elixir_lang.configuration.ElixirCompilerSettings;
import org.elixir_lang.icons.ElixirIcons;
import org.elixir_lang.mix.settings.MixSettings;
import org.elixir_lang.module.ElixirModuleType;
import org.elixir_lang.sdk.ElixirSdkType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* Created by zyuyou on 15/7/1.
* https://github.com/ignatov/intellij-erlang/blob/master/src/org/intellij/erlang/rebar/importWizard/RebarProjectImportBuilder.java
*/
public class MixProjectImportBuilder extends ProjectImportBuilder<ImportedOtpApp> {
private static final Logger LOG = Logger.getInstance(MixProjectImportBuilder.class);
private boolean myOpenProjectSettingsAfter = false;
@Nullable
private VirtualFile myProjectRoot = null;
@NotNull
private List<ImportedOtpApp> myFoundOtpApps = Collections.emptyList();
@NotNull private List<ImportedOtpApp> mySelectedOtpApps = Collections.emptyList();
@NotNull private String myMixPath = "";
private boolean myIsImportingProject;
@NotNull
@Override
public String getName() {
return "Mix";
}
@Override
public Icon getIcon() {
return ElixirIcons.MIX;
}
@Override
public boolean isSuitableSdkType(SdkTypeId sdkType) {
return sdkType == ElixirSdkType.getInstance();
}
@Override
public List<ImportedOtpApp> getList() {
return new ArrayList<ImportedOtpApp>(myFoundOtpApps);
}
@Override
public void setList(List<ImportedOtpApp> selectedOtpApps) throws ConfigurationException {
if(selectedOtpApps != null){
mySelectedOtpApps = selectedOtpApps;
}
}
@Override
public boolean isMarked(ImportedOtpApp importedOtpApp) {
return importedOtpApp != null && mySelectedOtpApps.contains(importedOtpApp);
}
@Override
public void setOpenProjectSettingsAfter(boolean openProjectSettingsAfter) {
myOpenProjectSettingsAfter = openProjectSettingsAfter;
}
@Override
public void cleanup() {
myOpenProjectSettingsAfter = false;
myProjectRoot = null;
myFoundOtpApps = Collections.emptyList();
mySelectedOtpApps = Collections.emptyList();
}
/**
* check for reusing *.iml or *.eml
*
* */
@SuppressWarnings("DialogTitleCapitalization")
@Override
public boolean validate(Project current, Project dest) {
if(!findIdeaModuleFiles(mySelectedOtpApps)){
return true;
}
int resultCode = Messages.showYesNoCancelDialog(ApplicationInfoEx.getInstanceEx().getFullApplicationName() + " module files found:\n\n" +
StringUtil.join(myFoundOtpApps, new Function<ImportedOtpApp, String>() {
@Override
public String fun(ImportedOtpApp importedOtpApp) {
VirtualFile ideaModuleFile = importedOtpApp.getIdeaModuleFile();
return ideaModuleFile != null ? " " + ideaModuleFile.getPath() + "\n" : "";
}
}, "") + "\nWould you like to reuse them?", "Module files found", Messages.getQuestionIcon());
if(resultCode == Messages.YES){
return true;
}else if(resultCode == Messages.NO){
try{
deleteIdeaModuleFiles(mySelectedOtpApps);
return true;
}catch (IOException e){
LOG.error(e);
return false;
}
}else {
return false;
}
}
@Override
public List<Module> commit(@NotNull Project project,
@Nullable ModifiableModuleModel moduleModel,
@NotNull ModulesProvider modulesProvider,
@Nullable ModifiableArtifactModel artifactModel) {
Set<String> selectedAppNames = ContainerUtil.newHashSet();
for (ImportedOtpApp importedOtpApp:mySelectedOtpApps){
selectedAppNames.add(importedOtpApp.getName());
}
Sdk projectSdk = fixProjectSdk(project);
List<Module> createModules = new ArrayList<Module>();
final List<ModifiableRootModel> createdRootModels = new ArrayList<ModifiableRootModel>();
final ModifiableModuleModel obtainedModuleModel =
moduleModel != null ? moduleModel : ModuleManager.getInstance(project).getModifiableModel();
VirtualFile _buildDir = null;
for (ImportedOtpApp importedOtpApp:mySelectedOtpApps){
// add Module
VirtualFile ideaModuleDir = importedOtpApp.getRoot();
String ideaModuleFile = ideaModuleDir.getCanonicalPath() + File.separator + importedOtpApp.getName() + ".iml";
Module module = obtainedModuleModel.newModule(ideaModuleFile, ElixirModuleType.getInstance().getId());
createModules.add(module);
// add rootModule
importedOtpApp.setModule(module);
if(importedOtpApp.getIdeaModuleFile() == null){
ModifiableRootModel rootModel = ModuleRootManager.getInstance(module).getModifiableModel();
// Make it inherit SDK from the project.
rootModel.inheritSdk();
// Initialize source and test paths.
ContentEntry content = rootModel.addContentEntry(importedOtpApp.getRoot());
addSourceDirToContent(content, ideaModuleDir, "lib", false);
addSourceDirToContent(content, ideaModuleDir, "test", true);
// Exclude standard folders
// excludeDirFromContent(content, ideaModuleDir, "_build");
// Initialize output paths according to mix conventions.
CompilerModuleExtension compilerModuleExt = rootModel.getModuleExtension(CompilerModuleExtension.class);
compilerModuleExt.inheritCompilerOutputPath(false);
_buildDir = myProjectRoot != null && myProjectRoot.equals(ideaModuleDir) ? ideaModuleDir : ideaModuleDir.getParent().getParent();
compilerModuleExt.setCompilerOutputPath(_buildDir + StringUtil.replace("/_build/dev/lib/" + importedOtpApp.getName() + "/ebin", "/", File.separator));
compilerModuleExt.setCompilerOutputPathForTests(_buildDir + StringUtil.replace("/_build/test/lib/" + importedOtpApp.getName() + "/ebin", "/", File.separator));
createdRootModels.add(rootModel);
// Set inter-module dependencies
resolveModuleDeps(rootModel, importedOtpApp, projectSdk, selectedAppNames);
}
}
// Commit project structure.
LOG.info("Commit project structure");
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
for (ModifiableRootModel rootModel : createdRootModels) {
rootModel.commit();
}
obtainedModuleModel.commit();
}
});
MixSettings.getInstance(project).setMixPath(myMixPath);
if(myIsImportingProject){
ElixirCompilerSettings.getInstance(project).setUseMixCompilerEnabled(true);
}
CompilerWorkspaceConfiguration.getInstance(project).CLEAR_OUTPUT_DIRECTORY = false;
return createModules;
}
public boolean setProjectRoot(@NotNull final VirtualFile projectRoot){
if(projectRoot.equals(myProjectRoot)){
return true;
}
boolean unitTestMode = ApplicationManager.getApplication().isUnitTestMode();
myProjectRoot = projectRoot;
if(!unitTestMode && projectRoot instanceof VirtualDirectoryImpl){
((VirtualDirectoryImpl)projectRoot).refreshAndFindChild("deps");
}
ProgressManager.getInstance().run(new Task.Modal(getCurrentProject(), "Scanning Mix Projects", true){
@Override
public void run(@NotNull final ProgressIndicator indicator) {
List<VirtualFile> mixExsFiles = findMixExs(myProjectRoot, indicator);
final LinkedHashSet<ImportedOtpApp> importedOtpApps = new LinkedHashSet<ImportedOtpApp>(mixExsFiles.size());
VfsUtilCore.visitChildrenRecursively(projectRoot, new VirtualFileVisitor() {
@Override
public boolean visitFile(@NotNull VirtualFile file) {
indicator.checkCanceled();
if(file.isDirectory()){
indicator.setText2(file.getPath());
if(isBuildOrConfigOrDepsOrTestsDirectory(projectRoot.getPath(), file.getPath())) return false;
}
ContainerUtil.addAllNotNull(importedOtpApps, createImportedOtpApp(file));
return true;
}
});
myFoundOtpApps = ContainerUtil.newArrayList(importedOtpApps);
}
});
Collections.sort(myFoundOtpApps, new Comparator<ImportedOtpApp>() {
@Override
public int compare(ImportedOtpApp o1, ImportedOtpApp o2) {
int nameCompareResult = String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName());
if (nameCompareResult == 0) {
return String.CASE_INSENSITIVE_ORDER.compare(o1.getRoot().getPath(), o1.getRoot().getPath());
}
return nameCompareResult;
}
});
mySelectedOtpApps = myFoundOtpApps;
return !myFoundOtpApps.isEmpty();
}
public void setMixPath(@NotNull String mixPath){
myMixPath = mixPath;
}
public void setIsImportingProject(boolean isImportingProject){
myIsImportingProject = isImportingProject;
}
/**
* private methos
* */
private static boolean isBuildOrConfigOrDepsOrTestsDirectory(String projectRootPath, String path){
return (projectRootPath + "/_build").equals(path)
|| (projectRootPath + "/config").equals(path)
|| (projectRootPath + "/deps").equals(path)
|| (projectRootPath + "/tests").equals(path);
}
@Nullable
private static Sdk fixProjectSdk(@NotNull Project project){
final ProjectRootManagerEx projectRootMgr = ProjectRootManagerEx.getInstanceEx(project);
Sdk selectedSdk = projectRootMgr.getProjectSdk();
if(selectedSdk == null || selectedSdk.getSdkType() != ElixirSdkType.getInstance()){
final Sdk moreSuitableSdk = ProjectJdkTable.getInstance().findMostRecentSdkOfType(ElixirSdkType.getInstance());
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
projectRootMgr.setProjectSdk(moreSuitableSdk);
}
});
return moreSuitableSdk;
}
return selectedSdk;
}
private static void addSourceDirToContent(@NotNull ContentEntry content,
@NotNull VirtualFile root,
@NotNull String sourceDir,
boolean test){
VirtualFile sourceDirFile = root.findChild(sourceDir);
if(sourceDirFile != null){
content.addSourceFolder(sourceDirFile, test);
}
}
private static void excludeDirFromContent(ContentEntry content, VirtualFile root, String excludeDir){
VirtualFile excludeDirFile = root.findChild(excludeDir);
if(excludeDirFile != null){
content.addExcludeFolder(excludeDirFile);
}
}
@NotNull
private List<VirtualFile> findMixExs(@NotNull final VirtualFile root, @NotNull final ProgressIndicator indicator){
// synchronous and recursive
root.refresh(false, true);
final List<VirtualFile> foundMixExs = new ArrayList<VirtualFile>();
VfsUtilCore.visitChildrenRecursively(root, new VirtualFileVisitor() {
@Override
public boolean visitFile(@NotNull VirtualFile file) {
indicator.checkCanceled();
if(file.isDirectory()){
if(isBuildOrConfigOrDepsOrTestsDirectory(root.getPath(), file.getPath())) return false;
indicator.setText2(file.getPath());
}else if(file.getName().equalsIgnoreCase("mix.exs")){
foundMixExs.add(file);
}
return true;
}
});
return foundMixExs;
}
@Nullable
private static ImportedOtpApp createImportedOtpApp(@NotNull VirtualFile appRoot){
VirtualFile appMixFile = appRoot.findChild("mix.exs");
if(appMixFile == null){
return null;
}
return new ImportedOtpApp(appRoot, appMixFile);
}
@Nullable
private static VirtualFile findFileByExtension(@NotNull VirtualFile dir, @NotNull String extension){
for(VirtualFile file : dir.getChildren()){
String fileName = file.getName();
if(!file.isDirectory() && fileName.endsWith(extension)){
return file;
}
}
return null; //To change body of created methods use File | settings | File Templates.
}
private static void deleteIdeaModuleFiles(@NotNull final List<ImportedOtpApp> importedOtpApps) throws IOException{
final IOException[] ex = new IOException[1];
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
for(ImportedOtpApp importedOtpApp : importedOtpApps){
VirtualFile ideaModuleFile = importedOtpApp.getIdeaModuleFile();
if(ideaModuleFile != null){
try{
ideaModuleFile.delete(this);
importedOtpApp.setIdeaModuleFile(null);
}catch (IOException e){
ex[0] = e;
}
}
}
}
});
if(ex[0] != null){
throw ex[0];
}
}
private static boolean findIdeaModuleFiles(@NotNull List<ImportedOtpApp> importedOtpApps){
boolean ideaModuleFileExists = false;
for (ImportedOtpApp importedOtpApp : importedOtpApps){
VirtualFile applicationRoot = importedOtpApp.getRoot();
String ideaModuleName = importedOtpApp.getName();
VirtualFile imlFile = applicationRoot.findChild(ideaModuleName + ".iml");
if(imlFile != null){
ideaModuleFileExists = true;
importedOtpApp.setIdeaModuleFile(imlFile);
}else{
VirtualFile emlFile = applicationRoot.findChild(ideaModuleName + ".eml");
if(emlFile != null){
ideaModuleFileExists = true;
importedOtpApp.setIdeaModuleFile(emlFile);
}
}
}
return ideaModuleFileExists;
}
@NotNull
private static Set<String> resolveModuleDeps(@NotNull ModifiableRootModel rootModel,
@NotNull ImportedOtpApp importedOtpApp,
@Nullable Sdk projectSdk,
@NotNull Set<String> allImportedAppNames){
HashSet<String> unresolvedAppNames = ContainerUtil.newHashSet();
for (String depAppName : importedOtpApp.getDeps()){
if(allImportedAppNames.contains(depAppName)){
rootModel.addInvalidModuleEntry(depAppName);
}else if(projectSdk != null && isSdkOtpApp(depAppName, projectSdk)){
// Sdk is already a dependency
LOG.info("Sdk otp-app:[" + depAppName + "] is already a dependy.");
}else{
rootModel.addInvalidModuleEntry(depAppName);
unresolvedAppNames.add(depAppName);
}
}
return unresolvedAppNames;
}
private static boolean isSdkOtpApp(@NotNull String otpAppName, @NotNull Sdk sdk){
for (VirtualFile sdkLibDir : sdk.getRootProvider().getFiles(OrderRootType.SOURCES)){
for(VirtualFile child: sdkLibDir.getChildren()){
if(child.isDirectory() && child.getName().equals(otpAppName)){
return true;
}
}
}
return false;
}
}