Merge IJ-MR-24582: [maven] collect cases when user has added module/library/folder...
[idea/community.git] / plugins / maven / src / main / java / org / jetbrains / idea / maven / importing / MavenRootModelAdapterLegacyImpl.java
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;
3
4 import com.intellij.openapi.application.ReadAction;
5 import com.intellij.openapi.externalSystem.ExternalSystemModulePropertyManager;
6 import com.intellij.openapi.module.Module;
7 import com.intellij.openapi.roots.*;
8 import com.intellij.openapi.roots.libraries.Library;
9 import com.intellij.openapi.util.Pair;
10 import com.intellij.openapi.util.Ref;
11 import com.intellij.openapi.util.registry.Registry;
12 import com.intellij.openapi.vfs.JarFileSystem;
13 import com.intellij.openapi.vfs.VfsUtilCore;
14 import com.intellij.openapi.vfs.VirtualFileManager;
15 import com.intellij.pom.java.LanguageLevel;
16 import org.apache.commons.lang.StringUtils;
17 import org.jetbrains.annotations.NotNull;
18 import org.jetbrains.annotations.Nullable;
19 import org.jetbrains.idea.maven.model.MavenArtifact;
20 import org.jetbrains.idea.maven.model.MavenConstants;
21 import org.jetbrains.idea.maven.project.MavenProject;
22 import org.jetbrains.idea.maven.project.MavenProjectsManager;
23 import org.jetbrains.idea.maven.statistics.MavenImportCollector;
24 import org.jetbrains.idea.maven.utils.MavenUtil;
25 import org.jetbrains.idea.maven.utils.Path;
26 import org.jetbrains.idea.maven.utils.Url;
27 import org.jetbrains.jps.model.JpsElement;
28 import org.jetbrains.jps.model.java.JavaSourceRootType;
29 import org.jetbrains.jps.model.java.JpsJavaExtensionService;
30 import org.jetbrains.jps.model.module.JpsModuleSourceRootType;
31
32 import java.io.File;
33 import java.nio.file.Files;
34 import java.nio.file.Paths;
35 import java.util.Arrays;
36 import java.util.HashSet;
37 import java.util.Set;
38
39 public class MavenRootModelAdapterLegacyImpl implements MavenRootModelAdapterInterface {
40
41   private final MavenProject myMavenProject;
42   private final ModuleModelProxy myModuleModel;
43   private final ModifiableRootModel myRootModel;
44
45   private final MavenSourceFoldersModuleExtension myRootModelModuleExtension;
46
47   private final Set<String> myOrderEntriesBeforeJdk = new HashSet<>();
48
49   public MavenRootModelAdapterLegacyImpl(@NotNull MavenProject p, @NotNull Module module, final ModifiableModelsProviderProxy rootModelsProvider) {
50     myMavenProject = p;
51     myModuleModel = rootModelsProvider.getModuleModelProxy();
52     myRootModel = rootModelsProvider.getModifiableRootModel(module);
53
54     myRootModelModuleExtension = myRootModel.getModuleExtension(MavenSourceFoldersModuleExtension.class);
55     myRootModelModuleExtension.init(module, myRootModel);
56   }
57
58   @Override
59   public void init(boolean isNewlyCreatedModule) {
60     setupInitialValues(isNewlyCreatedModule);
61     initContentRoots();
62     initOrderEntries();
63   }
64
65   private void setupInitialValues(boolean newlyCreatedModule) {
66     if (newlyCreatedModule || myRootModel.getSdk() == null) {
67       myRootModel.inheritSdk();
68     }
69     if (newlyCreatedModule) {
70       getCompilerExtension().setExcludeOutput(true);
71     }
72   }
73
74   private void initContentRoots() {
75     Url url = toUrl(myMavenProject.getDirectory());
76     if (getContentRootFor(url) != null) return;
77     myRootModel.addContentEntry(url.getUrl());
78   }
79
80   private ContentEntry getContentRootFor(Url url) {
81     for (ContentEntry e : myRootModel.getContentEntries()) {
82       if (VfsUtilCore.isEqualOrAncestor(e.getUrl(), url.getUrl())) return e;
83     }
84     return null;
85   }
86
87   private void initOrderEntries() {
88     boolean jdkProcessed = false;
89
90     for (OrderEntry e : myRootModel.getOrderEntries()) {
91       if (e instanceof ModuleSourceOrderEntry || e instanceof JdkOrderEntry) {
92         jdkProcessed = true;
93         continue;
94       }
95
96       if (e instanceof LibraryOrderEntry) {
97         if (Registry.is("maven.always.remove.bad.entries")) {
98           if (!isMavenLibrary((LibraryOrderEntry)e)) {
99             MavenImportCollector.HAS_USER_ADDED_LIBRARY_DEP.log(myRootModel.getProject());
100             continue;
101           }
102         }
103         else {
104           if (!isMavenLibrary(((LibraryOrderEntry)e).getLibrary())) {
105             MavenImportCollector.HAS_USER_ADDED_LIBRARY_DEP.log(myRootModel.getProject());
106             continue;
107           }
108         }
109       }
110       if (e instanceof ModuleOrderEntry) {
111         Module m = ((ModuleOrderEntry)e).getModule();
112         if (m != null &&
113             !MavenProjectsManager.getInstance(myRootModel.getProject()).isMavenizedModule(m) &&
114             ExternalSystemModulePropertyManager.getInstance(m).getExternalSystemId() == null) {
115           MavenImportCollector.HAS_USER_ADDED_MODULE_DEP.log(myRootModel.getProject());
116           continue;
117         }
118       }
119
120       if (!jdkProcessed) {
121         if (e instanceof ModuleOrderEntry) {
122           myOrderEntriesBeforeJdk.add(((ModuleOrderEntry)e).getModuleName());
123         }
124         else if (e instanceof LibraryOrderEntry) {
125           myOrderEntriesBeforeJdk.add(((LibraryOrderEntry)e).getLibraryName());
126         }
127       }
128
129       myRootModel.removeOrderEntry(e);
130     }
131   }
132
133   @Override
134   public ModifiableRootModel getRootModel() {
135     return myRootModel;
136   }
137
138   @Override
139   public String @NotNull [] getSourceRootUrls(boolean includingTests) {
140     return myRootModelModuleExtension.getSourceRootUrls(includingTests);
141   }
142
143   @Override
144   public Module getModule() {
145     return myRootModel.getModule();
146   }
147
148   @Override
149   public void clearSourceFolders() {
150     myRootModelModuleExtension.clearSourceFolders();
151   }
152
153   @Override
154   public <P extends JpsElement> void addSourceFolder(String path, final JpsModuleSourceRootType<P> rootType) {
155     addSourceFolder(path, rootType, false, rootType.createDefaultProperties());
156   }
157
158   @Override
159   public void addGeneratedJavaSourceFolder(String path, JavaSourceRootType rootType, boolean ifNotEmpty) {
160     addSourceFolder(path, rootType, ifNotEmpty, JpsJavaExtensionService.getInstance().createSourceRootProperties("", true));
161   }
162
163   @Override
164   public void addGeneratedJavaSourceFolder(String path, JavaSourceRootType rootType) {
165     addGeneratedJavaSourceFolder(path, rootType, true);
166   }
167
168   private  <P extends JpsElement> void addSourceFolder(@NotNull String path, final @NotNull JpsModuleSourceRootType<P> rootType, boolean ifNotEmpty,
169                                                        final @NotNull P properties) {
170     if (ifNotEmpty) {
171       String[] childs = new File(toPath(path).getPath()).list();
172       if (childs == null || childs.length == 0) return;
173     }
174     else {
175       if (!exists(path)) return;
176     }
177
178     Url url = toUrl(path);
179     myRootModelModuleExtension.addSourceFolder(url, rootType, properties);
180   }
181
182   @Override
183   public boolean hasRegisteredSourceSubfolder(@NotNull File f) {
184     String url = toUrl(f.getPath()).getUrl();
185     return myRootModelModuleExtension.hasRegisteredSourceSubfolder(url);
186   }
187
188   @Override
189   @Nullable
190   public SourceFolder getSourceFolder(File folder) {
191     String url = toUrl(folder.getPath()).getUrl();
192     return myRootModelModuleExtension.getSourceFolder(url);
193   }
194
195   @Override
196   public boolean isAlreadyExcluded(File f) {
197     String url = toUrl(f.getPath()).getUrl();
198     return VfsUtilCore.isUnder(url, Arrays.asList(myRootModel.getExcludeRootUrls()));
199   }
200
201   private boolean exists(String path) {
202     return Files.exists(Paths.get(toPath(path).getPath()));
203   }
204
205   @Override
206   public void addExcludedFolder(String path) {
207     unregisterAll(path, true, false);
208     Url url = toUrl(path);
209     ContentEntry e = getContentRootFor(url);
210     if (e == null) return;
211     if (e.getUrl().equals(url.getUrl())) return;
212     e.addExcludeFolder(url.getUrl());
213   }
214
215   @Override
216   public void unregisterAll(String path, boolean under, boolean unregisterSources) {
217     Url url = toUrl(path);
218
219     for (ContentEntry eachEntry : myRootModel.getContentEntries()) {
220       if (unregisterSources) {
221         myRootModelModuleExtension.unregisterAll(url, under);
222       }
223
224       for (String excludedUrl : eachEntry.getExcludeFolderUrls()) {
225         String ancestor = under ? url.getUrl() : excludedUrl;
226         String child = under ? excludedUrl : url.getUrl();
227
228         if (VfsUtilCore.isEqualOrAncestor(ancestor, child)) {
229           eachEntry.removeExcludeFolder(excludedUrl);
230         }
231       }
232       for (String outputUrl : getCompilerExtension().getOutputRootUrls(true)) {
233         String ancestor = under ? url.getUrl() : outputUrl;
234         String child = under ? outputUrl : url.getUrl();
235         if (VfsUtilCore.isEqualOrAncestor(ancestor, child)) {
236           getCompilerExtension().setExcludeOutput(false);
237         }
238       }
239     }
240   }
241
242   @Override
243   public boolean hasCollision(String sourceRootPath) {
244     Url url = toUrl(sourceRootPath);
245
246     for (ContentEntry eachEntry : myRootModel.getContentEntries()) {
247       for (SourceFolder eachFolder : eachEntry.getSourceFolders()) {
248         String ancestor = url.getUrl();
249         String child = eachFolder.getUrl();
250         if (VfsUtilCore.isEqualOrAncestor(ancestor, child) || VfsUtilCore.isEqualOrAncestor(child, ancestor)) {
251           return true;
252         }
253       }
254
255       for (String excludeUrl : eachEntry.getExcludeFolderUrls()) {
256         String ancestor = url.getUrl();
257         if (VfsUtilCore.isEqualOrAncestor(ancestor, excludeUrl) || VfsUtilCore.isEqualOrAncestor(excludeUrl, ancestor)) {
258           return true;
259         }
260       }
261     }
262
263     return false;
264   }
265
266   @Override
267   public void useModuleOutput(String production, String test) {
268     getCompilerExtension().inheritCompilerOutputPath(false);
269     if (StringUtils.isEmpty(production) && StringUtils.isEmpty(test)) {
270       getCompilerExtension().inheritCompilerOutputPath(true);
271     }
272     else if (StringUtils.isEmpty(test)) {
273       getCompilerExtension().setCompilerOutputPath(toUrl(production).getUrl());
274       getCompilerExtension().setExcludeOutput(true);
275     }
276     else if (StringUtils.isEmpty(production)) {
277       getCompilerExtension().setCompilerOutputPathForTests(toUrl(test).getUrl());
278       getCompilerExtension().setExcludeOutput(true);
279     }
280     else {
281       getCompilerExtension().setCompilerOutputPath(toUrl(production).getUrl());
282       getCompilerExtension().setCompilerOutputPathForTests(toUrl(test).getUrl());
283     }
284   }
285
286   private CompilerModuleExtension getCompilerExtension() {
287     return myRootModel.getModuleExtension(CompilerModuleExtension.class);
288   }
289
290   private Url toUrl(String path) {
291     return toPath(path).toUrl();
292   }
293
294   @Override
295   public Path toPath(String path) {
296     return MavenUtil.toPath(myMavenProject, path);
297   }
298
299   @Override
300   public void addModuleDependency(@NotNull String moduleName,
301                                   @NotNull DependencyScope scope,
302                                   boolean testJar) {
303     Module m = findModuleByName(moduleName);
304
305     ModuleOrderEntry e;
306     if (m != null) {
307       e = myRootModel.addModuleOrderEntry(m);
308     }
309     else {
310       e = ReadAction.compute(() -> myRootModel.addInvalidModuleEntry(moduleName));
311     }
312
313     e.setScope(scope);
314     if (testJar) {
315       e.setProductionOnTestDependency(true);
316     }
317
318     if (myOrderEntriesBeforeJdk.contains(moduleName)) {
319       moveLastOrderEntryBeforeJdk();
320     }
321   }
322
323   @Override
324   @Nullable
325   public Module findModuleByName(String moduleName) {
326     return myModuleModel.findModuleByName(moduleName);
327   }
328
329   @Override
330   public void addSystemDependency(MavenArtifact artifact, DependencyScope scope) {
331     assert MavenConstants.SCOPE_SYSTEM.equals(artifact.getScope());
332
333     String libraryName = artifact.getLibraryName();
334
335     Library library = myRootModel.getModuleLibraryTable().getLibraryByName(libraryName);
336     if (library == null) {
337       library = myRootModel.getModuleLibraryTable().createLibrary(libraryName);
338     }
339
340     LibraryOrderEntry orderEntry = myRootModel.findLibraryOrderEntry(library);
341     assert orderEntry != null;
342     orderEntry.setScope(scope);
343
344     Library.ModifiableModel modifiableModel = library.getModifiableModel();
345     updateUrl(modifiableModel, OrderRootType.CLASSES, artifact, null, null, true);
346     modifiableModel.commit();
347
348     if (myOrderEntriesBeforeJdk.contains(libraryName)) {
349       moveLastOrderEntryBeforeJdk();
350     }
351   }
352
353   @Override
354   public LibraryOrderEntry addLibraryDependency(MavenArtifact artifact,
355                                                 DependencyScope scope,
356                                                 ModifiableModelsProviderProxy provider,
357                                                 MavenProject project) {
358     assert !MavenConstants.SCOPE_SYSTEM.equals(artifact.getScope()); // System dependencies must be added ad module library, not as project wide library.
359
360     String libraryName = artifact.getLibraryName();
361
362     Library library = provider.getLibraryByName(libraryName);
363     if (library == null) {     library = provider.createLibrary(libraryName, getMavenExternalSource());
364     }
365     Library.ModifiableModel libraryModel = provider.getModifiableLibraryModel(library);
366
367     updateUrl(libraryModel, OrderRootType.CLASSES, artifact, null, null, true);
368     updateUrl(libraryModel, OrderRootType.SOURCES, artifact, MavenExtraArtifactType.SOURCES, project, false);
369     updateUrl(libraryModel, JavadocOrderRootType.getInstance(), artifact, MavenExtraArtifactType.DOCS, project, false);
370
371     LibraryOrderEntry e = myRootModel.addLibraryEntry(library);
372     e.setScope(scope);
373
374     if (myOrderEntriesBeforeJdk.contains(libraryName)) {
375       moveLastOrderEntryBeforeJdk();
376     }
377
378     return e;
379   }
380
381   private void moveLastOrderEntryBeforeJdk() {
382     OrderEntry[] entries = myRootModel.getOrderEntries().clone();
383
384     int i = entries.length - 1;
385     while (i > 0 && (entries[i - 1] instanceof ModuleSourceOrderEntry || entries[i - 1] instanceof JdkOrderEntry)) {
386       OrderEntry e = entries[i - 1];
387       entries[i - 1] = entries[i];
388       entries[i] = e;
389       i--;
390     }
391
392     if (i < entries.length) {
393       myRootModel.rearrangeOrderEntries(entries);
394     }
395   }
396
397   private static void updateUrl(Library.ModifiableModel library,
398                                 OrderRootType type,
399                                 MavenArtifact artifact,
400                                 MavenExtraArtifactType artifactType,
401                                 MavenProject project,
402                                 boolean clearAll) {
403     String classifier = null;
404     String extension = null;
405
406     if (artifactType != null) {
407       Pair<String, String> result = project.getClassifierAndExtension(artifact, artifactType);
408       classifier = result.first;
409       extension = result.second;
410     }
411
412
413     String newPath = artifact.getPathForExtraArtifact(classifier, extension);
414     String newUrl = VirtualFileManager.constructUrl(JarFileSystem.PROTOCOL, newPath) + JarFileSystem.JAR_SEPARATOR;
415
416     boolean urlExists = false;
417
418     for (String url : library.getUrls(type)) {
419       if (newUrl.equals(url)) {
420         urlExists = true;
421         continue;
422       }
423       if (clearAll || (isRepositoryUrl(artifact, url) && !url.startsWith(newUrl))) {
424         library.removeRoot(url, type);
425       }
426     }
427
428     if (!urlExists) {
429       library.addRoot(newUrl, type);
430     }
431   }
432
433   private static boolean isRepositoryUrl(MavenArtifact artifact, String url) {
434     return url.contains(artifact.getGroupId().replace('.', '/') + '/' + artifact.getArtifactId() + '/' + artifact.getBaseVersion() + '/' + artifact.getArtifactId() + '-');
435   }
436
437
438   @Override
439   public Library findLibrary(@NotNull final MavenArtifact artifact) {
440     final String name = artifact.getLibraryName();
441     final Ref<Library> result = Ref.create(null);
442     myRootModel.orderEntries().forEachLibrary(library -> {
443       if (name.equals(library.getName())) {
444         result.set(library);
445       }
446       return true;
447     });
448     return result.get();
449   }
450
451   public static boolean isMavenLibrary(@Nullable Library library) {
452     return library != null && MavenArtifact.isMavenLibrary(library.getName());
453   }
454
455   public static boolean isMavenLibrary(@Nullable LibraryOrderEntry entry) {
456     return entry != null && MavenArtifact.isMavenLibrary(entry.getLibraryName());
457   }
458
459   public static ProjectModelExternalSource getMavenExternalSource() {
460     return ExternalProjectSystemRegistry.getInstance().getSourceById(ExternalProjectSystemRegistry.MAVEN_EXTERNAL_SOURCE_ID);
461   }
462
463   @Nullable
464   public static OrderEntry findLibraryEntry(@NotNull Module m, @NotNull MavenArtifact artifact) {
465     String name = artifact.getLibraryName();
466     for (OrderEntry each : ModuleRootManager.getInstance(m).getOrderEntries()) {
467       if (each instanceof LibraryOrderEntry && name.equals(((LibraryOrderEntry)each).getLibraryName())) {
468         return each;
469       }
470     }
471     return null;
472   }
473
474   @Nullable
475   public static MavenArtifact findArtifact(@NotNull MavenProject project, @Nullable Library library) {
476     if (library == null) return null;
477
478     String name = library.getName();
479
480     if (!MavenArtifact.isMavenLibrary(name)) return null;
481
482     for (MavenArtifact each : project.getDependencies()) {
483       if (each.getLibraryName().equals(name)) return each;
484     }
485     return null;
486   }
487
488   @Override
489   public void setLanguageLevel(LanguageLevel level) {
490     try {
491       myRootModel.getModuleExtension(LanguageLevelModuleExtension.class).setLanguageLevel(level);
492     }
493     catch (IllegalArgumentException e) {
494       //bad value was stored
495     }
496   }
497 }