/*
* Copyright 2012-2015 Sergey Ignatov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.intellij.erlang.rebar.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.DialogWrapper;
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.VirtualFileManager;
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.containers.ContainerUtil;
import com.intellij.util.io.URLUtil;
import org.intellij.erlang.configuration.ErlangCompilerSettings;
import org.intellij.erlang.facet.ErlangFacet;
import org.intellij.erlang.facet.ErlangFacetConfiguration;
import org.intellij.erlang.icons.ErlangIcons;
import org.intellij.erlang.module.ErlangModuleType;
import org.intellij.erlang.rebar.settings.RebarSettings;
import org.intellij.erlang.roots.ErlangIncludeDirectoryUtil;
import org.intellij.erlang.sdk.ErlangSdkType;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
public class RebarProjectImportBuilder extends ProjectImportBuilder<ImportedOtpApp> {
private static final Logger LOG = Logger.getInstance(RebarProjectImportBuilder.class);
private boolean myOpenProjectSettingsAfter = false;
@Nullable private VirtualFile myProjectRoot = null;
@NotNull private List<ImportedOtpApp> myFoundOtpApps = Collections.emptyList();
@NotNull private List<ImportedOtpApp> mySelectedOtpApps = Collections.emptyList();
private boolean myImportExamples;
@NotNull private String myRebarPath = "";
private boolean myIsImportingProject;
@NotNull
@NonNls
@Override
public String getName() {
return "Rebar";
}
@Override
public Icon getIcon() {
return ErlangIcons.REBAR;
}
@Override
public boolean isSuitableSdkType(@NotNull SdkTypeId sdkType) {
return sdkType == ErlangSdkType.getInstance();
}
@Override
public List<ImportedOtpApp> getList() {
return new ArrayList<>(myFoundOtpApps);
}
@Override
public void setList(@Nullable List<ImportedOtpApp> selectedOtpApps) throws ConfigurationException {
if (selectedOtpApps != null) {
mySelectedOtpApps = selectedOtpApps;
}
}
@Override
public boolean isMarked(@Nullable ImportedOtpApp importedOtpApp) {
return importedOtpApp != null && mySelectedOtpApps.contains(importedOtpApp);
}
@Override
public boolean isOpenProjectSettingsAfter() {
return myOpenProjectSettingsAfter;
}
@Override
public void setOpenProjectSettingsAfter(boolean openProjectSettingsAfter) {
myOpenProjectSettingsAfter = openProjectSettingsAfter;
}
@Override
public void cleanup() {
myOpenProjectSettingsAfter = false;
myProjectRoot = null;
myFoundOtpApps = Collections.emptyList();
mySelectedOtpApps = Collections.emptyList();
}
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");
((VirtualDirectoryImpl) projectRoot).refreshAndFindChild("_build");
}
ProgressManager.getInstance().run(new Task.Modal(getCurrentProject(), "Scanning Rebar Projects", true) {
public void run(@NotNull final ProgressIndicator indicator) {
List<VirtualFile> rebarConfigFiles = findRebarConfigs(myProjectRoot, indicator);
final LinkedHashSet<ImportedOtpApp> importedOtpApps = new LinkedHashSet<>(rebarConfigFiles.size());
VfsUtilCore.visitChildrenRecursively(projectRoot, new VirtualFileVisitor() {
@Override
public boolean visitFile(@NotNull VirtualFile file) {
indicator.checkCanceled();
if (file.isDirectory()) {
indicator.setText2(file.getPath());
if (isExamplesDirectory(file) || isRelDirectory(projectRoot.getPath(), file.getPath())) return false;
String relativePath = VfsUtilCore.getRelativePath(file, projectRoot);
if ("_build/test".equals(relativePath)) return false;
}
ContainerUtil.addAllNotNull(importedOtpApps, createImportedOtpApp(file));
return true;
}
});
myFoundOtpApps = ContainerUtil.newArrayList(importedOtpApps);
}
});
Collections.sort(myFoundOtpApps, (o1, o2) -> {
int compareByParentPath = String.CASE_INSENSITIVE_ORDER.compare(o1.getRoot().getParent().getPath(), o2.getRoot().getParent().getPath());
if (compareByParentPath == 0) {
return String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName());
}
return compareByParentPath;
});
mySelectedOtpApps = myFoundOtpApps;
return !myFoundOtpApps.isEmpty();
}
private static boolean isRelDirectory(String projectRootPath, String path) {
return (projectRootPath + "/rel").equals(path);
}
@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(mySelectedOtpApps, importedOtpApp -> {
VirtualFile ideaModuleFile = importedOtpApp.getIdeaModuleFile();
return ideaModuleFile != null ? " " + ideaModuleFile.getPath() + "\n" : "";
}, "") +
"\nWould you like to reuse them?", "Module files found",
Messages.getQuestionIcon());
if (resultCode == DialogWrapper.OK_EXIT_CODE) {
return true;
}
else if (resultCode == DialogWrapper.CANCEL_EXIT_CODE) {
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 modifiableArtifactModel) {
Set<String> selectedAppNames = ContainerUtil.newHashSet();
for (ImportedOtpApp importedOtpApp : mySelectedOtpApps) {
selectedAppNames.add(importedOtpApp.getName());
}
Sdk projectSdk = fixProjectSdk(project);
List<Module> createdModules = new ArrayList<>();
final List<ModifiableRootModel> createdRootModels = new ArrayList<>();
final ModifiableModuleModel obtainedModuleModel =
moduleModel != null ? moduleModel : ModuleManager.getInstance(project).getModifiableModel();
for (ImportedOtpApp importedOtpApp : mySelectedOtpApps) {
VirtualFile ideaModuleDir = importedOtpApp.getRoot();
String ideaModuleDirPath = ideaModuleDir.getCanonicalPath();
String ideaModuleFile = ideaModuleDirPath + File.separator + importedOtpApp.getName() + ".iml";
Module module = obtainedModuleModel.newModule(ideaModuleFile, ErlangModuleType.getInstance().getId());
createdModules.add(module);
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, "src", false);
addSourceDirToContent(content, ideaModuleDir, "test", true);
addIncludeDirectories(content, importedOtpApp);
// Exclude standard folders
excludeDirFromContent(content, ideaModuleDir, "doc");
content.addExcludeFolder(VirtualFileManager.constructUrl(URLUtil.FILE_PROTOCOL, ideaModuleDirPath + File.separator + "_build") );
content.addExcludeFolder(VirtualFileManager.constructUrl(URLUtil.FILE_PROTOCOL, ideaModuleDirPath + File.separator + ".rebar3") );
// Initialize output paths according to Rebar conventions.
CompilerModuleExtension compilerModuleExt = rootModel.getModuleExtension(CompilerModuleExtension.class);
compilerModuleExt.inheritCompilerOutputPath(false);
compilerModuleExt.setCompilerOutputPath(ideaModuleDir + File.separator + "ebin");
compilerModuleExt.setCompilerOutputPathForTests(ideaModuleDir + File.separator + ".eunit");
createdRootModels.add(rootModel);
// Set inter-module dependencies
resolveModuleDeps(rootModel, importedOtpApp, projectSdk, selectedAppNames);
}
}
// Commit project structure.
LOG.info("Commit project structure");
ApplicationManager.getApplication().runWriteAction(() -> {
for (ModifiableRootModel rootModel : createdRootModels) {
rootModel.commit();
}
obtainedModuleModel.commit();
});
addErlangFacets(mySelectedOtpApps);
RebarSettings.getInstance(project).setRebarPath(myRebarPath);
if (myIsImportingProject) {
ErlangCompilerSettings.getInstance(project).setUseRebarCompilerEnabled(true);
}
CompilerWorkspaceConfiguration.getInstance(project).CLEAR_OUTPUT_DIRECTORY = false;
return createdModules;
}
private static void addErlangFacets(final List<ImportedOtpApp> apps) {
ApplicationManager.getApplication().runWriteAction(() -> {
for (ImportedOtpApp app : apps) {
Module module = app.getModule();
if (module == null) continue;
ErlangFacet facet = ErlangFacet.getFacet(module);
if (facet == null) {
ErlangFacet.createFacet(module);
facet = ErlangFacet.getFacet(module);
}
if (facet != null) {
ErlangFacetConfiguration configuration = facet.getConfiguration();
configuration.addParseTransforms(app.getParseTransforms());
}
}
});
}
@Nullable
private static Sdk fixProjectSdk(@NotNull Project project) {
final ProjectRootManagerEx projectRootMgr = ProjectRootManagerEx.getInstanceEx(project);
Sdk selectedSdk = projectRootMgr.getProjectSdk();
if (selectedSdk == null || selectedSdk.getSdkType() != ErlangSdkType.getInstance()) {
final Sdk moreSuitableSdk = ProjectJdkTable.getInstance().findMostRecentSdkOfType(ErlangSdkType.getInstance());
ApplicationManager.getApplication().runWriteAction(() -> 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 addIncludeDirectories(@NotNull ContentEntry content, ImportedOtpApp app) {
for (VirtualFile includeDirectory : app.getIncludePaths()) {
ErlangIncludeDirectoryUtil.markAsIncludeDirectory(content, includeDirectory);
}
}
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> findRebarConfigs(@NotNull final VirtualFile root, @NotNull final ProgressIndicator indicator) {
root.refresh(false, true);
final List<VirtualFile> foundRebarConfigs = new ArrayList<>();
VfsUtilCore.visitChildrenRecursively(root, new VirtualFileVisitor() {
@Override
public boolean visitFile(@NotNull VirtualFile file) {
indicator.checkCanceled();
if (file.isDirectory()) {
if (isExamplesDirectory(file) || isRelDirectory(root.getPath(), file.getPath())) return false;
indicator.setText2(file.getPath());
}
else if (file.getName().equalsIgnoreCase("rebar.config")) {
foundRebarConfigs.add(file);
}
return true;
}
});
return foundRebarConfigs;
}
private boolean isExamplesDirectory(VirtualFile virtualFile) {
return "examples".equals(virtualFile.getName()) && !myImportExamples;
}
@Nullable
private static ImportedOtpApp createImportedOtpApp(@NotNull VirtualFile appRoot) {
VirtualFile appResourceFile = findAppResourceFile(appRoot);
if (appResourceFile == null) {
return null;
}
return new ImportedOtpApp(appRoot, appResourceFile);
}
@Nullable
private static VirtualFile findAppResourceFile(@NotNull VirtualFile applicationRoot) {
VirtualFile appResourceFile = null;
VirtualFile sourceDir = applicationRoot.findChild("src");
if (sourceDir != null) {
appResourceFile = findFileByExtension(sourceDir, "app.src");
}
if (appResourceFile == null) {
VirtualFile ebinDir = applicationRoot.findChild("ebin");
if (ebinDir != null) {
appResourceFile = findFileByExtension(ebinDir, "app");
}
}
return appResourceFile;
}
@Nullable
private static VirtualFile findFileByExtension(@NotNull VirtualFile dir, @NotNull String extension) {
for (VirtualFile file : dir.getChildren()) {
if (!file.isDirectory() && file.getName().endsWith(extension)) return file;
}
return null;
}
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
}
else {
rootModel.addInvalidModuleEntry(depAppName);
unresolvedAppNames.add(depAppName);
}
}
return unresolvedAppNames;
}
private static boolean isSdkOtpApp(@NotNull String otpAppName, @NotNull Sdk sdk) {
Pattern appDirNamePattern = Pattern.compile(otpAppName + "-.*");
for (VirtualFile srcSdkDir : sdk.getRootProvider().getFiles(OrderRootType.SOURCES)) {
if (!srcSdkDir.isValid()) continue;
for (VirtualFile child : srcSdkDir.getChildren()) {
if (child.isDirectory() && appDirNamePattern.matcher(child.getName()).find()) {
return true;
}
}
}
return false;
}
public void setImportExamples(boolean importExamples) {
myImportExamples = importExamples;
}
public void setRebarPath(@NotNull String rebarPath) {
myRebarPath = rebarPath;
}
public void setIsImportingProject(boolean isImportingProject) {
myIsImportingProject = isImportingProject;
}
}