1 // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package org.jetbrains.idea.maven.importing;
4 import com.intellij.compiler.impl.javaCompiler.javac.JavacConfiguration;
5 import com.intellij.openapi.application.ApplicationManager;
6 import com.intellij.openapi.diagnostic.Logger;
7 import com.intellij.openapi.externalSystem.ExternalSystemModulePropertyManager;
8 import com.intellij.openapi.externalSystem.model.project.ProjectId;
9 import com.intellij.openapi.externalSystem.service.project.*;
10 import com.intellij.openapi.module.Module;
11 import com.intellij.openapi.module.ModuleType;
12 import com.intellij.openapi.module.ModuleWithNameAlreadyExists;
13 import com.intellij.openapi.module.impl.ModulePathKt;
14 import com.intellij.openapi.project.Project;
15 import com.intellij.openapi.roots.LibraryOrderEntry;
16 import com.intellij.openapi.roots.ModifiableRootModel;
17 import com.intellij.openapi.roots.ModuleRootModel;
18 import com.intellij.openapi.roots.OrderEntry;
19 import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
20 import com.intellij.openapi.roots.impl.libraries.LibraryEx;
21 import com.intellij.openapi.roots.libraries.Library;
22 import com.intellij.openapi.ui.Messages;
23 import com.intellij.openapi.util.Pair;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.openapi.vfs.LocalFileSystem;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.util.ArrayUtil;
28 import com.intellij.util.ArrayUtilRt;
29 import com.intellij.util.containers.ContainerUtil;
30 import com.intellij.util.containers.Stack;
31 import com.intellij.workspaceModel.ide.WorkspaceModel;
32 import com.intellij.workspaceModel.ide.legacyBridge.ModuleBridge;
33 import com.intellij.workspaceModel.storage.WorkspaceEntityStorage;
34 import com.intellij.workspaceModel.storage.WorkspaceEntityStorageBuilder;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37 import org.jetbrains.idea.maven.model.MavenId;
38 import org.jetbrains.idea.maven.project.*;
39 import org.jetbrains.idea.maven.utils.MavenLog;
40 import org.jetbrains.idea.maven.utils.MavenUtil;
41 import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerOptions;
43 import java.io.IOException;
46 import static org.jetbrains.idea.maven.project.MavenProjectChanges.ALL;
48 class MavenProjectImporterImpl extends MavenProjectImporterBase {
49 private static final Logger LOG = Logger.getInstance(MavenProjectImporterImpl.class);
50 private final Project myProject;
51 private final Map<VirtualFile, Module> myFileToModuleMapping;
52 private volatile Set<MavenProject> myAllProjects;
53 private final boolean myImportModuleGroupsRequired;
55 private final IdeModifiableModelsProvider myIdeModifiableModelsProvider;
56 private final WorkspaceEntityStorageBuilder myDiff;
57 private ModifiableModelsProviderProxy myModelsProvider;
58 private ModuleModelProxy myModuleModel;
59 private Module myDummyModule;
61 private final List<Module> myCreatedModules = new ArrayList<>();
63 private final Map<MavenProject, Module> myMavenProjectToModule = new HashMap<>();
64 private final Map<MavenProject, String> myMavenProjectToModuleName = new HashMap<>();
65 private final Map<MavenProject, String> myMavenProjectToModulePath = new HashMap<>();
67 MavenProjectImporterImpl(@NotNull Project p,
68 @NotNull MavenProjectsTree projectsTree,
69 @NotNull Map<MavenProject, MavenProjectChanges> projectsToImportWithChanges,
70 boolean importModuleGroupsRequired,
71 @NotNull IdeModifiableModelsProvider modelsProvider,
72 @NotNull MavenImportingSettings importingSettings,
73 @Nullable Module dummyModule) {
74 super(projectsTree, importingSettings, projectsToImportWithChanges);
76 myFileToModuleMapping = getFileToModuleMapping(p, dummyModule, modelsProvider);
77 myImportModuleGroupsRequired = importModuleGroupsRequired;
78 myDummyModule = dummyModule;
80 if (modelsProvider instanceof IdeModifiableModelsProviderImpl) {
81 IdeModifiableModelsProviderImpl impl = (IdeModifiableModelsProviderImpl)modelsProvider;
82 myDiff = impl.getActualStorageBuilder();
88 myIdeModifiableModelsProvider = modelsProvider;
93 public List<MavenProjectsProcessorTask> importProject() {
95 if (MavenUtil.newModelEnabled(myProject) && myDiff != null) {
96 myModelsProvider = new ModifiableModelsProviderProxyImpl(myProject, myDiff);
99 myModelsProvider = new ModifiableModelsProviderProxyWrapper(myIdeModifiableModelsProvider);
101 myModuleModel = myModelsProvider.getModuleModelProxy();
102 return importProjectOldWay();
106 private List<MavenProjectsProcessorTask> importProjectOldWay() {
107 List<MavenProjectsProcessorTask> postTasks = new ArrayList<>();
110 // in the case projects are changed during importing we must memorise them
111 myAllProjects = new LinkedHashSet<>(myProjectsTree.getProjects());
113 myAllProjects.addAll(myProjectsToImportWithChanges.keySet()); // some projects may already have been removed from the tree
115 hasChanges = deleteIncompatibleModules();
116 myProjectsToImportWithChanges = collectProjectsToImport(myProjectsToImportWithChanges);
118 mapMavenProjectsToModulesAndNames();
120 if (myProject.isDisposed()) return null;
122 final boolean projectsHaveChanges = projectsToImportHaveChanges();
123 final List<MavenModuleImporter> importers = new ArrayList<>();
124 if (projectsHaveChanges) {
126 importers.addAll(importModules());
127 scheduleRefreshResolvedArtifacts(postTasks);
130 if (projectsHaveChanges || myImportModuleGroupsRequired) {
132 configModuleGroups();
135 if (myProject.isDisposed()) return null;
137 List<Module> obsoleteModules = collectObsoleteModules();
138 boolean isDeleteObsoleteModules = isDeleteObsoleteModules(obsoleteModules);
139 hasChanges |= isDeleteObsoleteModules;
142 MavenUtil.invokeAndWaitWriteAction(myProject, () -> {
143 ProjectRootManagerEx.getInstanceEx(myProject).mergeRootsChangesDuring(() -> {
144 setMavenizedModules(obsoleteModules, false);
145 List<Module> toDelete = new ArrayList<>();
146 if (myDummyModule != null) {
147 toDelete.add(myDummyModule);
148 myDummyModule = null;
150 if (isDeleteObsoleteModules) {
151 toDelete.addAll(obsoleteModules);
153 deleteModules(toDelete);
154 removeUnusedProjectLibraries();
156 myModelsProvider.commit();
158 if (projectsHaveChanges) {
159 removeOutdatedCompilerConfigSettings();
162 if (projectsHaveChanges) {
163 setMavenizedModules(ContainerUtil.map(myProjectsToImportWithChanges.keySet(), myMavenProjectToModule::get), true);
168 if (!importers.isEmpty()) {
169 IdeModifiableModelsProvider provider;
170 if (myIdeModifiableModelsProvider instanceof IdeUIModifiableModelsProvider) {
171 provider = myIdeModifiableModelsProvider; // commit does nothing for this provider, so it should be reused
174 provider = ProjectDataManager.getInstance().createModifiableModelsProvider(myProject);
178 List<MavenModuleImporter> toRun = new ArrayList<>(importers.size());
179 for (MavenModuleImporter importer : importers) {
180 if (!importer.isModuleDisposed()) {
181 importer.setModifiableModelsProvider(provider);
185 configFacets(postTasks, toRun);
188 MavenUtil.invokeAndWaitWriteAction(myProject, () -> {
189 ProjectRootManagerEx.getInstanceEx(myProject).mergeRootsChangesDuring(() -> {
198 MavenUtil.invokeAndWaitWriteAction(myProject, () -> setMavenizedModules(obsoleteModules, false));
199 disposeModifiableModels();
205 private void disposeModifiableModels() {
206 MavenUtil.invokeAndWaitWriteAction(myProject, () -> myModelsProvider.dispose());
209 private Map<MavenProject, MavenProjectChanges> collectProjectsToImport(Map<MavenProject, MavenProjectChanges> projectsToImport) {
210 Map<MavenProject, MavenProjectChanges> result = new HashMap<>(projectsToImport);
211 result.putAll(collectNewlyCreatedProjects()); // e.g. when 'create modules fro aggregators' setting changes
213 Set<MavenProject> allProjectsToImport = result.keySet();
214 Set<MavenProject> selectedProjectsToImport = selectProjectsToImport(allProjectsToImport);
216 Iterator<MavenProject> it = allProjectsToImport.iterator();
217 while (it.hasNext()) {
218 if (!selectedProjectsToImport.contains(it.next())) it.remove();
224 private Map<MavenProject, MavenProjectChanges> collectNewlyCreatedProjects() {
225 Map<MavenProject, MavenProjectChanges> result = new HashMap<>();
227 for (MavenProject each : myAllProjects) {
228 Module module = myFileToModuleMapping.get(each.getFile());
229 if (module == null) {
230 result.put(each, ALL);
237 private boolean deleteIncompatibleModules() {
238 final Pair<List<Pair<MavenProject, Module>>, List<Pair<MavenProject, Module>>> incompatible = collectIncompatibleModulesWithProjects();
239 final List<Pair<MavenProject, Module>> incompatibleMavenized = incompatible.first;
240 final List<Pair<MavenProject, Module>> incompatibleNotMavenized = incompatible.second;
242 if (incompatibleMavenized.isEmpty() && incompatibleNotMavenized.isEmpty()) return false;
244 boolean changed = false;
246 // For already mavenized modules the type may change because maven project plugins were resolved and MavenImporter asked to create a module of a different type.
247 // In such cases we must change module type silently.
248 for (Pair<MavenProject, Module> each : incompatibleMavenized) {
249 myFileToModuleMapping.remove(each.first.getFile());
250 myModuleModel.disposeModule(each.second);
254 if (incompatibleNotMavenized.isEmpty()) return changed;
256 final int[] result = new int[]{Messages.OK};
257 if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
258 MavenUtil.invokeAndWait(myProject, myModelsProvider.getModalityStateForQuestionDialogs(), () -> {
259 String message = MavenProjectBundle.message("maven.import.incompatible.modules",
260 incompatibleNotMavenized.size(),
261 formatProjectsWithModules(incompatibleNotMavenized));
263 MavenProjectBundle.message("maven.import.incompatible.modules.recreate"),
264 MavenProjectBundle.message("maven.import.incompatible.modules.ignore")
267 result[0] = Messages.showOkCancelDialog(myProject, message,
268 MavenProjectBundle.message("maven.project.import.title"),
269 options[0], options[1], Messages.getQuestionIcon());
273 if (result[0] == Messages.OK) {
274 for (Pair<MavenProject, Module> each : incompatibleNotMavenized) {
275 myFileToModuleMapping.remove(each.first.getFile());
276 myModuleModel.disposeModule(each.second);
281 myProjectsTree.setIgnoredState(MavenUtil.collectFirsts(incompatibleNotMavenized), true, true);
288 * Collects modules that need to change module type
290 * @return the first List in returned Pair contains already mavenized modules, the second List - not mavenized
292 private Pair<List<Pair<MavenProject, Module>>, List<Pair<MavenProject, Module>>> collectIncompatibleModulesWithProjects() {
293 List<Pair<MavenProject, Module>> incompatibleMavenized = new ArrayList<>();
294 List<Pair<MavenProject, Module>> incompatibleNotMavenized = new ArrayList<>();
296 MavenProjectsManager manager = MavenProjectsManager.getInstance(myProject);
297 for (MavenProject each : myAllProjects) {
298 Module module = myFileToModuleMapping.get(each.getFile());
299 if (module == null) continue;
301 if (shouldCreateModuleFor(each) && !(ModuleType.get(module).equals(each.getModuleType()))) {
302 (manager.isMavenizedModule(module) ? incompatibleMavenized : incompatibleNotMavenized).add(Pair.create(each, module));
305 return Pair.create(incompatibleMavenized, incompatibleNotMavenized);
308 private static String formatProjectsWithModules(List<Pair<MavenProject, Module>> projectsWithModules) {
309 return StringUtil.join(projectsWithModules, each -> {
310 MavenProject project = each.first;
311 Module module = each.second;
312 return ModuleType.get(module).getName() +
315 "' for Maven project " +
316 project.getMavenId().getDisplayString();
320 private void deleteModules(@NotNull List<Module> modules) {
321 for (Module each : modules) {
322 if (!each.isDisposed()) {
323 myModuleModel.disposeModule(each);
328 private boolean isDeleteObsoleteModules(@NotNull List<Module> obsoleteModules) {
329 if (obsoleteModules.isEmpty()) {
332 if (!ApplicationManager.getApplication().isHeadlessEnvironment() || MavenUtil.isMavenUnitTestModeEnabled()) {
333 final int[] result = new int[1];
334 MavenUtil.invokeAndWait(myProject, myModelsProvider.getModalityStateForQuestionDialogs(),
335 () -> result[0] = Messages.showYesNoDialog(myProject,
336 MavenProjectBundle.message("maven.import.message.delete.obsolete",
337 formatModules(obsoleteModules)),
338 MavenProjectBundle.message("maven.project.import.title"),
339 Messages.getQuestionIcon()));
341 if (result[0] == Messages.NO) {
348 private List<Module> collectObsoleteModules() {
349 List<Module> remainingModules = new ArrayList<>();
350 Collections.addAll(remainingModules, myModuleModel.getModules());
352 for (MavenProject each : selectProjectsToImport(myAllProjects)) {
353 remainingModules.remove(myMavenProjectToModule.get(each));
356 List<Module> obsolete = new ArrayList<>();
357 final MavenProjectsManager manager = MavenProjectsManager.getInstance(myProject);
358 for (Module each : remainingModules) {
359 if (manager.isMavenizedModule(each) && myDummyModule != each) {
366 private static String formatModules(final Collection<Module> modules) {
367 StringBuilder res = new StringBuilder();
370 for (Module module : modules) {
371 res.append('\'').append(module.getName()).append("'\n");
377 res.append("\n ... and other ").append(modules.size() - 20).append(" modules");
380 return res.toString();
383 private void mapMavenProjectsToModulesAndNames() {
384 for (MavenProject each : myAllProjects) {
385 Module module = myFileToModuleMapping.get(each.getFile());
386 if (module != null) {
387 myMavenProjectToModule.put(each, module);
391 MavenModuleNameMapper.map(myAllProjects,
392 myMavenProjectToModule,
393 myMavenProjectToModuleName,
394 myMavenProjectToModulePath,
395 myImportingSettings.getDedicatedModuleDir());
398 private void removeOutdatedCompilerConfigSettings() {
399 ApplicationManager.getApplication().assertWriteAccessAllowed();
401 final JpsJavaCompilerOptions javacOptions = JavacConfiguration.getOptions(myProject, JavacConfiguration.class);
402 String options = javacOptions.ADDITIONAL_OPTIONS_STRING;
403 options = options.replaceFirst("(-target (\\S+))", ""); // Old IDEAs saved
404 javacOptions.ADDITIONAL_OPTIONS_STRING = options;
407 private List<MavenModuleImporter> importModules() {
408 Map<MavenProject, MavenProjectChanges> projectsWithChanges = myProjectsToImportWithChanges;
410 Set<MavenProject> projectsWithNewlyCreatedModules = new HashSet<>();
412 for (MavenProject each : projectsWithChanges.keySet()) {
413 if (ensureModuleCreated(each)) {
414 projectsWithNewlyCreatedModules.add(each);
418 List<MavenModuleImporter> importers = new ArrayList<>();
420 for (Map.Entry<MavenProject, MavenProjectChanges> each : projectsWithChanges.entrySet()) {
421 MavenProject project = each.getKey();
422 Module module = myMavenProjectToModule.get(project);
423 boolean isNewModule = projectsWithNewlyCreatedModules.contains(project);
424 MavenId mavenId = project.getMavenId();
425 myModelsProvider.registerModulePublication(
426 module, new ProjectId(mavenId.getGroupId(), mavenId.getArtifactId(), mavenId.getVersion()));
427 MavenModuleImporter moduleImporter = createModuleImporter(module, project, each.getValue());
428 importers.add(moduleImporter);
430 MavenRootModelAdapter rootModelAdapter =
431 new MavenRootModelAdapter(new MavenRootModelAdapterLegacyImpl(project, module, myModelsProvider));
432 rootModelAdapter.init(isNewModule);
433 moduleImporter.config(rootModelAdapter);
436 for (MavenProject project : myAllProjects) {
437 if (!projectsWithChanges.containsKey(project)) {
438 Module module = myMavenProjectToModule.get(project);
439 if (module == null) continue;
441 importers.add(createModuleImporter(module, project, null));
448 private void configFacets(List<MavenProjectsProcessorTask> tasks, List<MavenModuleImporter> importers) {
449 for (MavenModuleImporter importer : importers) {
450 importer.preConfigFacets();
453 for (MavenModuleImporter importer : importers) {
454 importer.configFacets(tasks);
457 for (MavenModuleImporter importer : importers) {
458 importer.postConfigFacets();
462 private void setMavenizedModules(final Collection<Module> modules, final boolean mavenized) {
463 ApplicationManager.getApplication().assertWriteAccessAllowed();
464 WorkspaceEntityStorage initialStorage = WorkspaceModel.getInstance(myProject).getEntityStorage().getCurrent();
465 WorkspaceEntityStorageBuilder storageBuilder = WorkspaceEntityStorageBuilder.from(initialStorage);
466 for (Module module : modules) {
467 if (module.isDisposed()) continue;
468 ExternalSystemModulePropertyManager modulePropertyManager = ExternalSystemModulePropertyManager.getInstance(module);
469 if (modulePropertyManager instanceof ExternalSystemModulePropertyManagerBridge &&
470 module instanceof ModuleBridge &&
471 ((ModuleBridge)module).getDiff() == null) {
472 ((ExternalSystemModulePropertyManagerBridge)modulePropertyManager).setMavenized(mavenized, storageBuilder);
475 modulePropertyManager.setMavenized(mavenized);
478 WorkspaceModel.getInstance(myProject).updateProjectModel(builder -> {
479 builder.addDiff(storageBuilder);
484 private boolean ensureModuleCreated(MavenProject project) {
485 Module existingModule = myMavenProjectToModule.get(project);
486 if (existingModule != null && existingModule != myDummyModule) return false;
487 final String path = myMavenProjectToModulePath.get(project);
488 String moduleName = ModulePathKt.getModuleNameByFilePath(path);
489 if (isForTheDummyModule(project, existingModule)) {
491 if (!myDummyModule.getName().equals(moduleName)) {
492 myModuleModel.renameModule(myDummyModule, moduleName);
495 catch (ModuleWithNameAlreadyExists e) {
496 MavenLog.LOG.error("Cannot rename dummy module:", e);
498 myMavenProjectToModule.put(project, myDummyModule);
499 myCreatedModules.add(myDummyModule);
500 myDummyModule = null;
505 // for some reason newModule opens the existing iml file, so we
506 // have to remove it beforehand.
507 deleteExistingImlFile(path);
508 deleteExistingModuleByName(moduleName);
509 final Module module = myModuleModel.newModule(path, project.getModuleType().getId());
511 myMavenProjectToModule.put(project, module);
512 myCreatedModules.add(module);
516 private boolean isForTheDummyModule(MavenProject project, Module existingModule) {
517 if (myDummyModule == null) return false;
518 if (existingModule == myDummyModule) return true;
519 return myProjectsTree.getRootProjects().size() == 1 &&
520 myProjectsTree.findRootProject(project) == project;
523 private void deleteExistingModuleByName(final String name) {
524 Module module = myModuleModel.findModuleByName(name);
525 if (module != null) {
526 myModuleModel.disposeModule(module);
530 private void deleteExistingImlFile(final String path) {
531 MavenUtil.invokeAndWaitWriteAction(myProject, new Runnable() {
535 VirtualFile file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path);
536 if (file != null) file.delete(this);
538 catch (IOException e) {
539 MavenLog.LOG.warn("Cannot delete existing iml file: " + path, e);
545 private MavenModuleImporter createModuleImporter(Module module, MavenProject mavenProject, @Nullable MavenProjectChanges changes) {
546 return new MavenModuleImporter(module,
550 myMavenProjectToModuleName,
555 private void configModuleGroups() {
556 if (!myImportingSettings.isCreateModuleGroups()) return;
558 final Stack<String> groups = new Stack<>();
559 final boolean createTopLevelGroup = myProjectsTree.getRootProjects().size() > 1;
561 myProjectsTree.visit(new MavenProjectsTree.SimpleVisitor() {
565 public boolean shouldVisit(MavenProject project) {
566 // in case some project has been added while we were importing
567 return myMavenProjectToModuleName.containsKey(project);
571 public void visit(MavenProject each) {
574 String name = myMavenProjectToModuleName.get(each);
576 if (shouldCreateGroup(each)) {
577 groups.push(MavenProjectBundle.message("module.group.name", name));
580 if (!shouldCreateModuleFor(each)) {
584 Module module = myModuleModel.findModuleByName(name);
585 if (module == null) return;
586 myModuleModel.setModuleGroupPath(module, groups.isEmpty() ? null : ArrayUtilRt.toStringArray(groups));
590 public void leave(MavenProject each) {
591 if (shouldCreateGroup(each)) {
597 private boolean shouldCreateGroup(MavenProject project) {
598 return !myProjectsTree.getModules(project).isEmpty()
599 && (createTopLevelGroup || depth > 1);
604 private boolean removeUnusedProjectLibraries() {
605 Set<Library> unusedLibraries = new HashSet<>();
606 Collections.addAll(unusedLibraries, myModelsProvider.getAllLibraries());
608 for (ModuleRootModel eachModel : collectModuleModels()) {
609 for (OrderEntry eachEntry : eachModel.getOrderEntries()) {
610 if (eachEntry instanceof LibraryOrderEntry) {
611 unusedLibraries.remove(((LibraryOrderEntry)eachEntry).getLibrary());
616 boolean removed = false;
617 for (Library each : unusedLibraries) {
618 if (!isDisposed(each) && MavenRootModelAdapter.isMavenLibrary(each) && !MavenRootModelAdapter.isChangedByUser(each)) {
619 myModelsProvider.removeLibrary(each);
626 private static boolean isDisposed(Library library) {
627 return library instanceof LibraryEx && ((LibraryEx)library).isDisposed();
630 private Collection<ModuleRootModel> collectModuleModels() {
631 Map<Module, ModuleRootModel> rootModels = new HashMap<>();
632 for (MavenProject each : myProjectsToImportWithChanges.keySet()) {
633 Module module = myMavenProjectToModule.get(each);
634 ModifiableRootModel rootModel = myModelsProvider.getModifiableRootModel(module);
635 rootModels.put(module, rootModel);
637 for (Module each : myModuleModel.getModules()) {
638 if (rootModels.containsKey(each)) continue;
639 rootModels.put(each, myModelsProvider.getModifiableRootModel(each));
641 return rootModels.values();
645 public @NotNull List<Module> createdModules() {
646 return myCreatedModules;
649 private static Map<VirtualFile, Module> getFileToModuleMapping(
651 Module myDummyModule,
652 IdeModifiableModelsProvider modelsProvider) {
653 return MavenProjectsManager.getInstance(project)
654 .getFileToModuleMapping(new MavenModelsProvider() {
656 public Module[] getModules() {
657 return ArrayUtil.remove(modelsProvider.getModules(), myDummyModule);
661 public VirtualFile[] getContentRoots(Module module) {
662 return modelsProvider.getContentRoots(module);