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