package de.janthomae.leiningenplugin.module; import clojure.lang.LazySeq; import com.intellij.ide.highlighter.ModuleFileType; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.application.Result; import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.module.ModifiableModuleModel; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.module.StdModuleTypes; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.*; import com.intellij.openapi.roots.ex.ProjectRootManagerEx; import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable; import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.roots.libraries.LibraryTable; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.*; import de.janthomae.leiningenplugin.leiningen.LeiningenAPI; import de.janthomae.leiningenplugin.utils.ClassPathUtils; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Created with IntelliJ IDEA. * User: Chris Shellenbarger * Date: 12/6/12 * Time: 5:53 AM * <p/> * Class used to assist in creation an IDEA module. Extracted out of the project creation for reusability and testing purposes. */ public class ModuleCreationUtils { public final static String LEIN_COMPILE_PATH = "compile-path"; public final static String LEIN_RESOURCE_PATHS = "resource-paths"; public final static String LEIN_SOURCE_PATHS = "source-paths"; public final static String LEIN_JAVA_SOURCE_PATHS = "java-source-paths"; public final static String LEIN_TEST_PATHS = "test-paths"; public final static String LEIN_PROJECT_NAME = "name"; public final static String LEIN_PROJECT_VERSION = "version"; public final static String LEIN_PROJECT_GROUP = "group"; /** * Default Constructor - intentionally side-effect free. */ public ModuleCreationUtils() { } /** * This function returns a list of virtual files pointing to the paths supporting a particular type (as defined in a leiningen project file: "resource-paths", "test-paths", and "source-paths" are examples). * It extracts them based off the corresponding values in leinProjectMap. * * @param type The type of paths to extract from the project. Corresponds with the names of the respective keys in a leiningen project file. Examples: "resource-paths", "test-paths", or "source-paths" * @param leinProjectMap The map to extract values from. * @return A list of paths to folders of type. */ public List<String> getPaths(String type, Map leinProjectMap) { LazySeq pathStrings = ((LazySeq) leinProjectMap.get(type)); List<String> results = new ArrayList<String>(); if (pathStrings != null) { for (Object obj : pathStrings) { String path = (String) obj; results.add(path); } } return results; } /** * Internal method used to add absolute paths to a content entry. * <p/> * This is done as part of support for multiple source entries. * <p/> * SIDE-EFFECT: Will modify contentEntry * * Note: This function will only add values to the content entry if they exist on the file system. If the path doesn't * exist on the file system, then it will be ignored. * * @param contentEntry The contentEntry to be updated * @param paths The list of paths to add. These need to be absolute paths. * @param isTestSource Indicate if this is a test directory */ protected void addSourceFoldersToContentEntry(final ContentEntry contentEntry, final List<String> paths, final boolean isTestSource) { for (final String path : paths) { new WriteAction() { @Override protected void run(Result result) throws Throwable { VirtualFile directory = LocalFileSystem.getInstance().refreshAndFindFileByPath(path); if (directory != null) { contentEntry.addSourceFolder(directory, isTestSource); } } }.execute(); } } /** * Update the contentEntry with the following values from the leinProjectMap added as source directories. * - "resource-paths" * - "source-paths" * - "test-paths" * * @param contentEntry The contentEntry to update. * @param leinProjectMap The map to extract values from. * @return The contentEntry updated with the sourcePaths added. */ public ContentEntry updateSourceAndResourcesPaths(ContentEntry contentEntry, Map leinProjectMap) { List<String> resourcePaths = getPaths(LEIN_RESOURCE_PATHS, leinProjectMap); addSourceFoldersToContentEntry(contentEntry, resourcePaths, false); List<String> sourcePaths = getPaths(LEIN_SOURCE_PATHS, leinProjectMap); addSourceFoldersToContentEntry(contentEntry, sourcePaths, false); List<String> javaSourcePaths = getPaths(LEIN_JAVA_SOURCE_PATHS,leinProjectMap); addSourceFoldersToContentEntry(contentEntry,javaSourcePaths,false); List<String> testPaths = getPaths(LEIN_TEST_PATHS, leinProjectMap); addSourceFoldersToContentEntry(contentEntry, testPaths, true); return contentEntry; } /** * Update the compiler extension to have the appropriate paths as configured in the project map. * <p/> * SIDE-EFFECT: Changes state of extension * * @param extension The compiler extension. * @param leinProjectMap The map to extract values from. * @return The compiler extension updated with the given settings. */ public CompilerModuleExtension updateCompilePath(final CompilerModuleExtension extension, Map leinProjectMap) { final String outputPathString = (String) leinProjectMap.get(LEIN_COMPILE_PATH); new WriteAction() { @Override protected void run(Result result) throws Throwable { try { VirtualFile outputPath = VfsUtil.createDirectoryIfMissing(outputPathString); extension.inheritCompilerOutputPath(false); extension.setCompilerOutputPath(outputPath); extension.setCompilerOutputPathForTests(outputPath); } catch (IOException e) { throw new RuntimeException("Could not create output directory" + outputPathString); } } }.execute(); return extension; } /** * Utility method to obtain the root model of the module. * <p/> * This performs a read of the iml file so we can reconstruct the state. * * @param module The module to obtain the root model for. * @return The root model of the module. */ public ModifiableRootModel getRootModel(final Module module) { return new ReadAction<ModifiableRootModel>() { protected void run(Result<ModifiableRootModel> result) throws Throwable { result.setResult(ModuleRootManager.getInstance(module).getModifiableModel()); } }.execute().getResultObject(); } /** * Private utility to create the module manager, which manages the list of modules that this project knows about. * * @param ideaProject The idea project. * @return The Model for the module manager. */ private ModifiableModuleModel createModuleManager(final Project ideaProject) { return new ReadAction<ModifiableModuleModel>() { protected void run(Result<ModifiableModuleModel> result) throws Throwable { result.setResult(ModuleManager.getInstance(ideaProject).getModifiableModel()); } }.execute().getResultObject(); } /** * Create or find the modules root model. * <p/> * If there exists a module with a matching name, that will be returned. * <p/> * Otherwise, we'll create a new module and add it to the moduleManager. * * @param moduleManager The module manager * @param name The name of the module * @return The module's modifiable model */ public ModifiableRootModel createModule(ModifiableModuleModel moduleManager, String workingDir, String name) { Module module = moduleManager.findModuleByName(name); if (module == null) { // oh-kay we don't have a module yet. String filePath = workingDir + File.separator + FileUtil.sanitizeFileName(name) + ModuleFileType.DOT_DEFAULT_EXTENSION; module = moduleManager.newModule(filePath, StdModuleTypes.JAVA.getId()); } return getRootModel(module); } /** * Initialize the source, resources, test, and compile paths on module. * * @param projectMap The leiningen project map. * @param module The module to update * @param contentRoot The virtual file pointing to the leiningen project root directory. (Usually where the project.clj file is) */ public void initializeModulePaths(Map projectMap, ModifiableRootModel module, VirtualFile contentRoot) { //Set up the paths module.inheritSdk(); final ContentEntry contentEntry = module.addContentEntry(contentRoot); //Maven doesn't let you have source files that aren't configured in the pom.xml for consistency reasons. //We'll apply the same laws to leiningen projects. contentEntry.clearSourceFolders(); //Add the source and resource paths to the module updateSourceAndResourcesPaths(contentEntry, projectMap); //Handle the compile path (output) CompilerModuleExtension compilerExtension = module.getModuleExtension(CompilerModuleExtension.class); updateCompilePath(compilerExtension, projectMap); } /** * Initialize the dependencies for the module. This will add any dependencies to the list of project libraries and * then add those libraries to the module via Order Entries. * * * @param module The module to update. * @param projectLibraries The list of project libraries. * @param dependencyMaps The list of maps containing the dependency information. * @return The set of libraries that were created. */ private List<LibraryInfo> initializeDependencies(ModifiableRootModel module, LibraryTable.ModifiableModel projectLibraries, List dependencyMaps) { //Reset them module's library order entries here - this actually happens in org.jetbrains.idea.maven.importing.MavenRootModelAdapter.initOrderEntries() for (OrderEntry orderEntry : module.getOrderEntries()) { if (orderEntry instanceof LibraryOrderEntry) { //Remove any library from the project list so it can be refreshed. Library library = ((LibraryOrderEntry) orderEntry).getLibrary(); if (library != null) { projectLibraries.removeLibrary(library); } module.removeOrderEntry(orderEntry); } } //Add the dependencies to the projects's library table - this is how maven does it - but we could put the libraries directly on the module - but maybe it's better if we share a lot of libraries between modules. List<LibraryInfo> libraries = createLibraries(projectLibraries, dependencyMaps); //Now add the libraries to the modules. for (LibraryInfo entry : libraries) { module.addLibraryEntry(entry.library).setScope(entry.dependencyScope); } return libraries; } /** * This method imports a leiningen module from a leiningen project file and imports it into the idea project. * <p/> * Notes: * <p/> * Each of the IDEA components has a getModifiableModel on it. This method returns a new instance each time you * invoke it. Once you have a modifiable model of the component you wish to update, you mutate it to the state * you wish. Once you're done, you call commit() on the modifiable model and it updates the component it came from. * <p/> * Since a lot of the components are persisted in files, commit() updates these files as well. Therefore you need * to make any calls to commit() from within a WriteAction. * * @param ideaProject The IDEA project to add the leiningen module to. * @param leinProjectFile The leiningen project file * @return The leiningen project map. */ public Map importModule(Project ideaProject, VirtualFile leinProjectFile) { ClassPathUtils.getInstance().switchToPluginClassLoader(); Map projectMap = LeiningenAPI.loadProject(leinProjectFile.getPath()); String name = (String) projectMap.get(LEIN_PROJECT_NAME); final ModifiableModuleModel moduleManager = createModuleManager(ideaProject); final ModifiableRootModel module = createModule(moduleManager, leinProjectFile.getParent().getPath(), name); initializeModulePaths(projectMap, module, leinProjectFile.getParent()); ProjectRootManagerEx rootManager = ProjectRootManagerEx.getInstanceEx(ideaProject); module.setSdk(rootManager.getProjectSdk()); //Setup the dependencies // Based loosely on org.jetbrains.idea.maven.importing.MavenRootModelAdapter#addLibraryDependency //We could use the module table here, but then the libraries wouldn't be shared across modules. final LibraryTable.ModifiableModel projectLibraries = ProjectLibraryTable.getInstance(ideaProject).getModifiableModel(); //Load all the dependencies from the project file List dependencyMaps = LeiningenAPI.loadDependencies(leinProjectFile.getCanonicalPath()); final List<LibraryInfo> dependencies = initializeDependencies(module, projectLibraries,dependencyMaps); new WriteAction() { @Override protected void run(Result result) throws Throwable { for (LibraryInfo library : dependencies) { library.modifiableModel.commit(); } //Save the project libraries projectLibraries.commit(); //Save the module itself to the module file. module.commit(); //Save the list of modules that are in this project to the IDEA project file moduleManager.commit(); } }.execute(); return projectMap; } /** * Create the libraries for the list of dependency maps. * <p/> * This will add the libraries to the libraryTable (if they don't already exist) and return a map of libraries mapped to their scopes as used in the module. * * @param libraryTable The library table to add the libraries to. * @param dependencyMaps The list of dependency maps definining the libraries needed. * @return A Map of the Libraries which were described in dependencyMaps along with their scope for the module */ private List<LibraryInfo> createLibraries(LibraryTable.ModifiableModel libraryTable, List dependencyMaps) { List<LibraryInfo> result = new ArrayList<LibraryInfo>(); final String LEIN_LIB_PREFIX = "Leiningen"; for (Object obj : dependencyMaps) { Map dependency = (Map) obj; //Check if the library already exists String libraryName = LEIN_LIB_PREFIX + ": " + dependency.get("groupid") + ":" + dependency.get("artifactid") + ":" + dependency.get("version"); Library library = libraryTable.getLibraryByName(libraryName); if (library == null) { library = libraryTable.createLibrary(libraryName); } // Add the library to a library model, which represents the data for a single library. Library.ModifiableModel libraryModel = library.getModifiableModel(); //Right now only deal with classes - a lot of clojure libraries have the .clj in them and not in a separate file //Remove existing classes as this is what maven does - you need to declare the dependencies in the project file for (String url : libraryModel.getUrls(OrderRootType.CLASSES)) { libraryModel.removeRoot(url, OrderRootType.CLASSES); } File file = ((File) dependency.get("file")); String path = file.getAbsolutePath(); String url = VirtualFileManager.constructUrl(JarFileSystem.PROTOCOL, path) + JarFileSystem.JAR_SEPARATOR; libraryModel.addRoot(url, OrderRootType.CLASSES); DependencyScope scope = determineScope((String) dependency.get("scope")); LibraryInfo libraryInfo = new LibraryInfo(); libraryInfo.library = library; libraryInfo.modifiableModel = libraryModel; libraryInfo.dependencyScope = scope; result.add(libraryInfo); } return result; } /** * Do a check to determine the proper scope. * * @param s The string scope. * @return A DependencyScope object */ private DependencyScope determineScope(String s) { //Issue 35: If the scope that is on the dependency doesn't match one of the DependencyScope types, then default to compile scope. DependencyScope scope = DependencyScope.COMPILE; if (s.equalsIgnoreCase("compile")) { scope = DependencyScope.COMPILE; } if (s.equalsIgnoreCase("test")) { scope = DependencyScope.TEST; } if (s.equalsIgnoreCase("runtime")) { scope = DependencyScope.RUNTIME; } if (s.equalsIgnoreCase("provided")) { scope = DependencyScope.PROVIDED; } return scope; } private static class LibraryInfo { public Library library; public Library.ModifiableModel modifiableModel; public DependencyScope dependencyScope; } }