0b8e5717e2ea5d5e69387fe612060842d9de94dc
[idea/community.git] / java / idea-ui / src / com / intellij / ide / util / importProject / ModuleInsight.java
1 // Copyright 2000-2020 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 com.intellij.ide.util.importProject;
3
4 import com.intellij.ide.JavaUiBundle;
5 import com.intellij.ide.util.projectWizard.importSources.DetectedProjectRoot;
6 import com.intellij.ide.util.projectWizard.importSources.DetectedSourceRoot;
7 import com.intellij.ide.util.projectWizard.importSources.impl.ProjectFromSourcesBuilderImpl;
8 import com.intellij.openapi.diagnostic.Logger;
9 import com.intellij.openapi.progress.ProcessCanceledException;
10 import com.intellij.openapi.progress.ProgressIndicator;
11 import com.intellij.openapi.util.Ref;
12 import com.intellij.openapi.util.io.FileUtil;
13 import com.intellij.openapi.util.io.FileUtilRt;
14 import com.intellij.util.Consumer;
15 import com.intellij.util.containers.ContainerUtil;
16 import com.intellij.util.containers.Interner;
17 import com.intellij.util.text.StringFactory;
18 import gnu.trove.TObjectIntHashMap;
19 import org.jetbrains.annotations.NotNull;
20 import org.jetbrains.annotations.Nullable;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.util.*;
25 import java.util.function.Predicate;
26
27 /**
28  * @author Eugene Zhuravlev
29  */
30 public abstract class ModuleInsight {
31   private static final Logger LOG = Logger.getInstance(ModuleInsight.class);
32   @NotNull protected final ProgressIndicatorWrapper myProgress;
33
34   private final Set<File> myEntryPointRoots = new HashSet<>();
35   private final List<DetectedSourceRoot> mySourceRoots = new ArrayList<>();
36   private final Set<String> myIgnoredNames = new HashSet<>();
37
38   private final Map<File, Set<String>> mySourceRootToReferencedPackagesMap = new HashMap<>();
39   private final Map<File, Set<String>> mySourceRootToPackagesMap = new HashMap<>();
40   private final Map<File, Set<String>> myJarToPackagesMap = new HashMap<>();
41   private final Interner<String> myInterner = Interner.createStringInterner();
42
43   private List<ModuleDescriptor> myModules;
44   private List<LibraryDescriptor> myLibraries;
45   private final Set<String> myExistingModuleNames;
46   private final Set<String> myExistingProjectLibraryNames;
47
48   public ModuleInsight(@Nullable final ProgressIndicator progress, Set<String> existingModuleNames, Set<String> existingProjectLibraryNames) {
49     myExistingModuleNames = existingModuleNames;
50     myExistingProjectLibraryNames = existingProjectLibraryNames;
51     myProgress = new ProgressIndicatorWrapper(progress);
52     setRoots(Collections.emptyList(), Collections.emptyList(), Collections.emptySet());
53   }
54
55   public final void setRoots(final List<? extends File> contentRoots, final List<? extends DetectedSourceRoot> sourceRoots, final Set<String> ignoredNames) {
56     myModules = null;
57     myLibraries = null;
58
59     myEntryPointRoots.clear();
60     myEntryPointRoots.addAll(contentRoots);
61
62     mySourceRoots.clear();
63     mySourceRoots.addAll(sourceRoots);
64
65     myIgnoredNames.clear();
66     myIgnoredNames.addAll(ignoredNames);
67
68     myJarToPackagesMap.clear();
69     myInterner.clear();
70   }
71
72   @Nullable
73   public List<LibraryDescriptor> getSuggestedLibraries() {
74     return myLibraries;
75   }
76
77   @Nullable
78   public List<ModuleDescriptor> getSuggestedModules() {
79     return myModules;
80   }
81
82   public void scanModules() {
83     myProgress.setIndeterminate(true);
84     final Map<File, ModuleDescriptor> contentRootToModules = new HashMap<>();
85
86     try {
87       myProgress.pushState();
88
89       List<DetectedSourceRoot> processedRoots = new ArrayList<>();
90       for (DetectedSourceRoot root : getSourceRootsToScan()) {
91         final File sourceRoot = root.getDirectory();
92         if (isIgnoredName(sourceRoot)) {
93           continue;
94         }
95         myProgress.setText("Scanning " + sourceRoot.getPath());
96
97         final HashSet<String> usedPackages = new HashSet<>();
98         mySourceRootToReferencedPackagesMap.put(sourceRoot, usedPackages);
99
100         final HashSet<String> selfPackages = new HashSet<>();
101         addExportedPackages(sourceRoot, selfPackages);
102
103         scanSources(sourceRoot, ProjectFromSourcesBuilderImpl.getPackagePrefix(root), usedPackages, selfPackages) ;
104         usedPackages.removeAll(selfPackages);
105         processedRoots.add(root);
106       }
107       myProgress.popState();
108
109       myProgress.pushState();
110       myProgress.setText("Building modules layout...");
111       Map<File, ModuleCandidate> rootToModule = new HashMap<>();
112       for (DetectedSourceRoot sourceRoot : processedRoots) {
113         final File srcRoot = sourceRoot.getDirectory();
114         final File moduleContentRoot = isEntryPointRoot(srcRoot) ? srcRoot : srcRoot.getParentFile();
115         rootToModule.computeIfAbsent(moduleContentRoot, file -> new ModuleCandidate(moduleContentRoot)).myRoots.add(sourceRoot);
116       }
117       maximizeModuleFolders(rootToModule.values());
118       for (Map.Entry<File, ModuleCandidate> entry : rootToModule.entrySet()) {
119         File root = entry.getKey();
120         ModuleCandidate module = entry.getValue();
121         ModuleDescriptor moduleDescriptor = createModuleDescriptor(module.myFolder, module.myRoots);
122         contentRootToModules.put(root, moduleDescriptor);
123       }
124
125       buildModuleDependencies(contentRootToModules);
126
127       myProgress.popState();
128     }
129     catch (ProcessCanceledException ignored) {
130     }
131
132     addModules(contentRootToModules.values());
133   }
134
135   private static final class ModuleCandidate {
136     final List<DetectedSourceRoot> myRoots = new ArrayList<>();
137     @NotNull File myFolder;
138
139     private ModuleCandidate(@NotNull File folder) {
140       myFolder = folder;
141     }
142   }
143
144   private void maximizeModuleFolders(@NotNull Collection<ModuleCandidate> modules) {
145     TObjectIntHashMap<File> dirToChildRootCount = new TObjectIntHashMap<>();
146     for (ModuleCandidate module : modules) {
147       walkParents(module.myFolder, this::isEntryPointRoot, file -> {
148         if (!dirToChildRootCount.adjustValue(file, 1)) {
149           dirToChildRootCount.put(file, 1);
150         }
151       });
152     }
153     for (ModuleCandidate module : modules) {
154       File moduleRoot = module.myFolder;
155       Ref<File> adjustedRootRef = new Ref<>(module.myFolder);
156       File current = moduleRoot;
157       while (dirToChildRootCount.get(current) == 1) {
158         adjustedRootRef.set(current);
159         if (isEntryPointRoot(current)) break;
160         current = current.getParentFile();
161       }
162       module.myFolder = adjustedRootRef.get();
163     }
164   }
165
166   private static void walkParents(@NotNull File file, Predicate<File> stopCondition, @NotNull Consumer<File> fileConsumer) {
167     File current = file;
168     while (true) {
169       fileConsumer.consume(current);
170       if (stopCondition.test(current)) break;
171       current = current.getParentFile();
172     }
173   }
174
175   protected void addExportedPackages(File sourceRoot, Set<String> packages) {
176     mySourceRootToPackagesMap.put(sourceRoot, packages);
177   }
178
179   protected boolean isIgnoredName(File sourceRoot) {
180     return myIgnoredNames.contains(sourceRoot.getName());
181   }
182
183   protected void addModules(Collection<? extends ModuleDescriptor> newModules) {
184     if (myModules == null) {
185       myModules = new ArrayList<>(newModules);
186     }
187     else {
188       myModules.addAll(newModules);
189     }
190     final Set<String> moduleNames = new HashSet<>(myExistingModuleNames);
191     for (ModuleDescriptor module : newModules) {
192       final String suggested = suggestUniqueName(moduleNames, module.getName());
193       module.setName(suggested);
194       moduleNames.add(suggested);
195     }
196   }
197
198   @NotNull
199   protected List<DetectedSourceRoot> getSourceRootsToScan() {
200     return Collections.unmodifiableList(mySourceRoots);
201   }
202
203   protected boolean isEntryPointRoot(File srcRoot) {
204     return myEntryPointRoots.contains(srcRoot);
205   }
206
207   protected abstract ModuleDescriptor createModuleDescriptor(final File moduleContentRoot, Collection<DetectedSourceRoot> sourceRoots);
208
209   private void buildModuleDependencies(final Map<File, ModuleDescriptor> contentRootToModules) {
210     final Set<File> moduleContentRoots = contentRootToModules.keySet();
211
212     for (File contentRoot : moduleContentRoots) {
213       final ModuleDescriptor checkedModule = contentRootToModules.get(contentRoot);
214       myProgress.setText2(JavaUiBundle.message("progress.details.building.library.dependencies.for.module", checkedModule.getName()));
215       buildJarDependencies(checkedModule);
216
217       myProgress.setText2(JavaUiBundle.message("progress.details.building.module.dependencies.for.module", checkedModule.getName()));
218       for (File aContentRoot : moduleContentRoots) {
219         final ModuleDescriptor aModule = contentRootToModules.get(aContentRoot);
220         if (checkedModule.equals(aModule)) {
221           continue; // avoid self-dependencies
222         }
223         final Collection<? extends DetectedProjectRoot> aModuleRoots = aModule.getSourceRoots();
224         checkModules:
225         for (DetectedProjectRoot srcRoot: checkedModule.getSourceRoots()) {
226           final Set<String> referencedBySourceRoot = mySourceRootToReferencedPackagesMap.get(srcRoot.getDirectory());
227           for (DetectedProjectRoot aSourceRoot : aModuleRoots) {
228             if (ContainerUtil.intersects(referencedBySourceRoot, mySourceRootToPackagesMap.get(aSourceRoot.getDirectory()))) {
229               checkedModule.addDependencyOn(aModule);
230               break checkModules;
231             }
232           }
233         }
234       }
235     }
236   }
237
238   private void buildJarDependencies(final ModuleDescriptor module) {
239     for (File jarFile : myJarToPackagesMap.keySet()) {
240       final Set<String> jarPackages = myJarToPackagesMap.get(jarFile);
241       for (DetectedProjectRoot srcRoot : module.getSourceRoots()) {
242         if (ContainerUtil.intersects(mySourceRootToReferencedPackagesMap.get(srcRoot.getDirectory()), jarPackages)) {
243           module.addLibraryFile(jarFile);
244           break;
245         }
246       }
247     }
248   }
249
250   public void scanLibraries() {
251     myProgress.setIndeterminate(true);
252     myProgress.pushState();
253     try {
254       try {
255         for (File root : myEntryPointRoots) {
256           myProgress.setText(JavaUiBundle.message("progress.text.scanning.for.libraries", root.getPath()));
257           scanRootForLibraries(root);
258         }
259       }
260       catch (ProcessCanceledException ignored) {
261       }
262       myProgress.setText(JavaUiBundle.message("progress.text.building.initial.libraries.layout"));
263       final List<LibraryDescriptor> libraries = buildInitialLibrariesLayout(myJarToPackagesMap.keySet());
264       // correct library names so that there are no duplicates
265       final Set<String> libNames = new HashSet<>(myExistingProjectLibraryNames);
266       for (LibraryDescriptor library : libraries) {
267         final Collection<File> libJars = library.getJars();
268         final String newName = suggestUniqueName(libNames, libJars.size() == 1 ? FileUtilRt
269           .getNameWithoutExtension(libJars.iterator().next().getName()) : library.getName());
270         library.setName(newName);
271         libNames.add(newName);
272       }
273       myLibraries = libraries;
274     }
275     finally {
276       myProgress.popState();
277     }
278   }
279
280   public abstract boolean isApplicableRoot(final DetectedProjectRoot root);
281
282   private static String suggestUniqueName(Set<String> existingNames, String baseName) {
283     String name = baseName;
284     int index = 1;
285     while (existingNames.contains(name)) {
286       name = baseName + (index++);
287     }
288     return name;
289   }
290
291   public void merge(final ModuleDescriptor mainModule, final ModuleDescriptor module) {
292     for (File contentRoot : module.getContentRoots()) {
293       final File _contentRoot = appendContentRoot(mainModule, contentRoot);
294       final Collection<DetectedSourceRoot> sources = module.getSourceRoots(contentRoot);
295       for (DetectedSourceRoot source : sources) {
296         mainModule.addSourceRoot(_contentRoot, source);
297       }
298     }
299     for (File jar : module.getLibraryFiles()) {
300       mainModule.addLibraryFile(jar);
301     }
302     // fix forward dependencies
303     for (ModuleDescriptor dependency : module.getDependencies()) {
304       if (!mainModule.equals(dependency)) { // avoid self-dependencies
305         mainModule.addDependencyOn(dependency);
306       }
307     }
308
309     myModules.remove(module);
310     // fix back dependencies
311     for (ModuleDescriptor moduleDescr : myModules) {
312       if (moduleDescr.getDependencies().contains(module)) {
313         moduleDescr.removeDependencyOn(module);
314         if (!moduleDescr.equals(mainModule)) { // avoid self-dependencies
315           moduleDescr.addDependencyOn(mainModule);
316         }
317       }
318     }
319   }
320
321   public LibraryDescriptor splitLibrary(LibraryDescriptor library, String newLibraryName, final Collection<? extends File> jarsToExtract) {
322     final LibraryDescriptor newLibrary = new LibraryDescriptor(newLibraryName, new ArrayList<>(jarsToExtract));
323     myLibraries.add(newLibrary);
324     library.removeJars(jarsToExtract);
325     if (library.getJars().size() == 0) {
326       removeLibrary(library);
327     }
328     return newLibrary;
329   }
330
331   @Nullable
332   public ModuleDescriptor splitModule(final ModuleDescriptor descriptor, String newModuleName, final Collection<? extends File> contentsToExtract) {
333     ModuleDescriptor newModule = null;
334     for (File root : contentsToExtract) {
335       final Collection<DetectedSourceRoot> sources = descriptor.removeContentRoot(root);
336       if (newModule == null) {
337         newModule = createModuleDescriptor(root, sources != null ? sources : new HashSet<>());
338       }
339       else {
340         if (sources != null && sources.size() > 0) {
341           for (DetectedSourceRoot source : sources) {
342             newModule.addSourceRoot(root, source);
343           }
344         }
345         else {
346           newModule.addContentRoot(root);
347         }
348       }
349     }
350
351     if (newModule != null) {
352       newModule.setName(newModuleName);
353       myModules.add(newModule);
354     }
355     else {
356       return null;
357     }
358
359     final Map<File, ModuleDescriptor> contentRootToModule = new HashMap<>();
360     for (ModuleDescriptor module : myModules) {
361       final Set<File> roots = module.getContentRoots();
362       for (File root : roots) {
363         contentRootToModule.put(root, module);
364       }
365       module.clearModuleDependencies();
366       module.clearLibraryFiles();
367     }
368
369     buildModuleDependencies(contentRootToModule);
370     return newModule;
371   }
372
373   public void removeLibrary(LibraryDescriptor lib) {
374     myLibraries.remove(lib);
375   }
376
377   public void moveJarsToLibrary(final LibraryDescriptor from, Collection<? extends File> files, LibraryDescriptor to) {
378     to.addJars(files);
379     from.removeJars(files);
380     // remove the library if it became empty
381     if (from.getJars().size() == 0) {
382       removeLibrary(from);
383     }
384   }
385
386   public Collection<LibraryDescriptor> getLibraryDependencies(ModuleDescriptor module) {
387     return getLibraryDependencies(module, myLibraries);
388   }
389
390   public static Collection<LibraryDescriptor> getLibraryDependencies(ModuleDescriptor module,
391                                                                      @Nullable List<? extends LibraryDescriptor> allLibraries) {
392     final Set<LibraryDescriptor> libs = new HashSet<>();
393     if (allLibraries != null) {
394       for (LibraryDescriptor library : allLibraries) {
395         if (ContainerUtil.intersects(library.getJars(), module.getLibraryFiles())) {
396           libs.add(library);
397         }
398       }
399     }
400     return libs;
401   }
402
403   private static File appendContentRoot(final ModuleDescriptor module, final File contentRoot) {
404     final Set<File> moduleRoots = module.getContentRoots();
405     for (File moduleRoot : moduleRoots) {
406       if (FileUtil.isAncestor(moduleRoot, contentRoot, false)) {
407         return moduleRoot; // no need to include a separate root
408       }
409       if (FileUtil.isAncestor(contentRoot, moduleRoot, true)) {
410         final Collection<DetectedSourceRoot> currentSources = module.getSourceRoots(moduleRoot);
411         module.removeContentRoot(moduleRoot);
412         module.addContentRoot(contentRoot);
413         for (DetectedSourceRoot source : currentSources) {
414           module.addSourceRoot(contentRoot, source);
415         }
416         return contentRoot; // no need to include a separate root
417       }
418     }
419     module.addContentRoot(contentRoot);
420     return contentRoot;
421   }
422
423
424   private static List<LibraryDescriptor> buildInitialLibrariesLayout(final Set<? extends File> jars) {
425     final Map<File, LibraryDescriptor> rootToLibraryMap = new HashMap<>();
426     for (File jar : jars) {
427       final File parent = jar.getParentFile();
428       LibraryDescriptor lib = rootToLibraryMap.get(parent);
429       if (lib == null) {
430         lib = new LibraryDescriptor(parent.getName(), new HashSet<>());
431         rootToLibraryMap.put(parent, lib);
432       }
433       lib.addJars(Collections.singleton(jar));
434     }
435     return new ArrayList<>(rootToLibraryMap.values());
436   }
437
438   private void scanSources(final File fromRoot, final String parentPackageName, final Set<? super String> usedPackages, final Set<? super String> selfPackages) {
439     if (isIgnoredName(fromRoot)) {
440       return;
441     }
442     final File[] files = fromRoot.listFiles();
443     if (files != null) {
444       myProgress.checkCanceled();
445       boolean includeParentName = false;
446       for (File file : files) {
447         if (file.isDirectory()) {
448           String subPackageName = parentPackageName + (parentPackageName.isEmpty() ? "" : ".") + file.getName();
449           scanSources(file, subPackageName, usedPackages, selfPackages);
450         }
451         else {
452           if (isSourceFile(file)) {
453             includeParentName = true;
454             scanSourceFile(file, usedPackages);
455           }
456         }
457       }
458       if (includeParentName) {
459         selfPackages.add(myInterner.intern(parentPackageName));
460       }
461     }
462   }
463
464   protected abstract boolean isSourceFile(final File file);
465
466   private void scanSourceFile(File file, final Set<? super String> usedPackages) {
467     myProgress.setText2(file.getName());
468     try {
469       final char[] chars = FileUtil.loadFileText(file);
470       scanSourceFileForImportedPackages(StringFactory.createShared(chars), s -> usedPackages.add(myInterner.intern(s)));
471     }
472     catch (IOException e) {
473       LOG.info(e);
474     }
475   }
476
477   protected abstract void scanSourceFileForImportedPackages(final CharSequence chars, Consumer<String> result);
478
479   private void scanRootForLibraries(File fromRoot) {
480     if (isIgnoredName(fromRoot)) {
481       return;
482     }
483     final File[] files = fromRoot.listFiles();
484     if (files != null) {
485       myProgress.checkCanceled();
486       for (File file : files) {
487         if (file.isDirectory()) {
488           scanRootForLibraries(file);
489         }
490         else {
491           final String fileName = file.getName();
492           if (isLibraryFile(fileName)) {
493             if (!myJarToPackagesMap.containsKey(file)) {
494               final HashSet<String> libraryPackages = new HashSet<>();
495               myJarToPackagesMap.put(file, libraryPackages);
496
497               myProgress.pushState();
498               myProgress.setText2(file.getName());
499               try {
500                 scanLibraryForDeclaredPackages(file, s -> {
501                   if (!libraryPackages.contains(s)) {
502                     libraryPackages.add(myInterner.intern(s));
503                   }
504                 });
505               }
506               catch (IOException e) {
507                 LOG.info(e);
508               }
509               catch (IllegalArgumentException e) { // may be thrown from java.util.zip.ZipCoder.toString for corrupted archive
510                 LOG.info(e);
511               }
512               catch (InternalError e) { // indicates that file is somehow damaged and cannot be processed
513                 LOG.info(e);
514               }
515               finally {
516                 myProgress.popState();
517               }
518             }
519           }
520         }
521       }
522     }
523   }
524
525   protected abstract boolean isLibraryFile(final String fileName);
526
527   protected abstract void scanLibraryForDeclaredPackages(File file, Consumer<String> result) throws IOException;
528
529 }