d002832282caa64da7526c18f9250e808cdceeb6
[idea/community.git] / platform / projectModel-impl / src / com / intellij / openapi / roots / impl / ProjectRootManagerImpl.java
1 // Copyright 2000-2019 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.openapi.roots.impl;
3
4 import com.intellij.openapi.Disposable;
5 import com.intellij.openapi.application.ApplicationManager;
6 import com.intellij.openapi.application.WriteAction;
7 import com.intellij.openapi.components.PersistentStateComponent;
8 import com.intellij.openapi.components.State;
9 import com.intellij.openapi.diagnostic.Logger;
10 import com.intellij.openapi.module.Module;
11 import com.intellij.openapi.module.ModuleManager;
12 import com.intellij.openapi.project.Project;
13 import com.intellij.openapi.projectRoots.ProjectJdkTable;
14 import com.intellij.openapi.projectRoots.Sdk;
15 import com.intellij.openapi.roots.*;
16 import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
17 import com.intellij.openapi.roots.libraries.Library;
18 import com.intellij.openapi.roots.libraries.LibraryTable;
19 import com.intellij.openapi.util.Disposer;
20 import com.intellij.openapi.util.EmptyRunnable;
21 import com.intellij.openapi.vfs.VfsUtilCore;
22 import com.intellij.openapi.vfs.VirtualFile;
23 import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
24 import com.intellij.util.EventDispatcher;
25 import com.intellij.util.containers.ContainerUtil;
26 import com.intellij.util.io.URLUtil;
27 import org.jdom.Element;
28 import org.jetbrains.annotations.ApiStatus;
29 import org.jetbrains.annotations.NotNull;
30 import org.jetbrains.jps.model.module.JpsModuleSourceRootType;
31
32 import java.util.*;
33
34 @State(name = "ProjectRootManager")
35 public class ProjectRootManagerImpl extends ProjectRootManagerEx implements PersistentStateComponent<Element> {
36   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.projectRoots.impl.ProjectRootManagerImpl");
37
38   private static final String PROJECT_JDK_NAME_ATTR = "project-jdk-name";
39   private static final String PROJECT_JDK_TYPE_ATTR = "project-jdk-type";
40   private static final String ATTRIBUTE_VERSION = "version";
41
42   protected final Project myProject;
43
44   private final EventDispatcher<ProjectJdkListener> myProjectJdkEventDispatcher = EventDispatcher.create(ProjectJdkListener.class);
45
46   private String myProjectSdkName;
47   private String myProjectSdkType;
48
49   private final OrderRootsCache myRootsCache;
50
51   protected boolean myStartupActivityPerformed;
52
53   private final RootProviderChangeListener myRootProviderChangeListener = new RootProviderChangeListener();
54
55   protected class BatchSession {
56     private final boolean myFileTypes;
57     private int myBatchLevel;
58     private boolean myChanged;
59
60     private BatchSession(final boolean fileTypes) {
61       myFileTypes = fileTypes;
62     }
63
64     protected void levelUp() {
65       if (myBatchLevel == 0) {
66         myChanged = false;
67       }
68       myBatchLevel += 1;
69     }
70
71     protected void levelDown() {
72       myBatchLevel -= 1;
73       if (myChanged && myBatchLevel == 0) {
74         try {
75           WriteAction.run(() -> fireRootsChanged(myFileTypes));
76         }
77         finally {
78           myChanged = false;
79         }
80       }
81     }
82
83     public void beforeRootsChanged() {
84       if (myBatchLevel == 0 || !myChanged) {
85         if (fireBeforeRootsChanged(myFileTypes)) {
86           myChanged = true;
87         }
88       }
89     }
90
91     public void rootsChanged() {
92       if (myBatchLevel == 0) {
93         if (fireRootsChanged(myFileTypes)) {
94           myChanged = false;
95         }
96       }
97     }
98   }
99
100   protected final BatchSession myRootsChanged = new BatchSession(false);
101   protected final BatchSession myFileTypesChanged = new BatchSession(true);
102   private final VirtualFilePointerListener myRootsValidityChangedListener = new VirtualFilePointerListener(){};
103
104   public static ProjectRootManagerImpl getInstanceImpl(Project project) {
105     return (ProjectRootManagerImpl)getInstance(project);
106   }
107
108   public ProjectRootManagerImpl(@NotNull Project project) {
109     myProject = project;
110     myRootsCache = new OrderRootsCache(project);
111     myJdkTableMultiListener = new JdkTableMultiListener(project);
112   }
113
114   @Override
115   @NotNull
116   public ProjectFileIndex getFileIndex() {
117     return ProjectFileIndex.SERVICE.getInstance(myProject);
118   }
119
120   @Override
121   @NotNull
122   public List<String> getContentRootUrls() {
123     final List<String> result = new ArrayList<>();
124     for (Module module : getModuleManager().getModules()) {
125       ContainerUtil.addAll(result, ModuleRootManager.getInstance(module).getContentRootUrls());
126     }
127     return result;
128   }
129
130   @Override
131   @NotNull
132   public VirtualFile[] getContentRoots() {
133     final List<VirtualFile> result = new ArrayList<>();
134     Module[] modules = getModuleManager().getModules();
135     for (Module module : modules) {
136       VirtualFile[] contentRoots = ModuleRootManager.getInstance(module).getContentRoots();
137       if (modules.length == 1) {
138         return contentRoots;
139       }
140
141       ContainerUtil.addAll(result, contentRoots);
142     }
143     return VfsUtilCore.toVirtualFileArray(result);
144   }
145
146   @NotNull
147   @Override
148   public VirtualFile[] getContentSourceRoots() {
149     final List<VirtualFile> result = new ArrayList<>();
150     for (Module module : getModuleManager().getModules()) {
151       final VirtualFile[] sourceRoots = ModuleRootManager.getInstance(module).getSourceRoots();
152       ContainerUtil.addAll(result, sourceRoots);
153     }
154     return VfsUtilCore.toVirtualFileArray(result);
155   }
156
157   @NotNull
158   @Override
159   public List<VirtualFile> getModuleSourceRoots(@NotNull Set<? extends JpsModuleSourceRootType<?>> rootTypes) {
160     List<VirtualFile> roots = new ArrayList<>();
161     for (Module module : getModuleManager().getModules()) {
162       roots.addAll(ModuleRootManager.getInstance(module).getSourceRoots(rootTypes));
163     }
164     return roots;
165   }
166
167   @NotNull
168   @Override
169   public OrderEnumerator orderEntries() {
170     return new ProjectOrderEnumerator(myProject, myRootsCache);
171   }
172
173   @NotNull
174   @Override
175   public OrderEnumerator orderEntries(@NotNull Collection<? extends Module> modules) {
176     return new ModulesOrderEnumerator(modules);
177   }
178
179   @NotNull
180   @Override
181   public VirtualFile[] getContentRootsFromAllModules() {
182     List<VirtualFile> result = new ArrayList<>();
183     final Module[] modules = getModuleManager().getSortedModules();
184     for (Module module : modules) {
185       final VirtualFile[] files = ModuleRootManager.getInstance(module).getContentRoots();
186       ContainerUtil.addAll(result, files);
187     }
188     ContainerUtil.addIfNotNull(result, myProject.getBaseDir());
189     return VfsUtilCore.toVirtualFileArray(result);
190   }
191
192   @Override
193   public Sdk getProjectSdk() {
194     if (myProjectSdkName == null) {
195       return null;
196     }
197
198     ProjectJdkTable projectJdkTable = ProjectJdkTable.getInstance();
199     if (myProjectSdkType == null) {
200       return projectJdkTable.findJdk(myProjectSdkName);
201     }
202     else {
203       return projectJdkTable.findJdk(myProjectSdkName, myProjectSdkType);
204     }
205   }
206
207   @Override
208   public String getProjectSdkName() {
209     return myProjectSdkName;
210   }
211
212   @Override
213   public void setProjectSdk(Sdk sdk) {
214     ApplicationManager.getApplication().assertWriteAccessAllowed();
215     if (sdk == null) {
216       myProjectSdkName = null;
217       myProjectSdkType = null;
218     }
219     else {
220       myProjectSdkName = sdk.getName();
221       myProjectSdkType = sdk.getSdkType().getName();
222     }
223     projectJdkChanged();
224   }
225
226   private void projectJdkChanged() {
227     incModificationCount();
228     mergeRootsChangesDuring(() -> myProjectJdkEventDispatcher.getMulticaster().projectJdkChanged());
229     Sdk sdk = getProjectSdk();
230     for (ProjectExtension extension : ProjectExtension.EP_NAME.getExtensions(myProject)) {
231       extension.projectSdkChanged(sdk);
232     }
233   }
234
235   @Override
236   public void setProjectSdkName(@NotNull String name) {
237     ApplicationManager.getApplication().assertWriteAccessAllowed();
238     myProjectSdkName = name;
239
240     projectJdkChanged();
241   }
242
243   @Override
244   public void addProjectJdkListener(@NotNull ProjectJdkListener listener) {
245     myProjectJdkEventDispatcher.addListener(listener);
246   }
247
248   @Override
249   public void removeProjectJdkListener(@NotNull ProjectJdkListener listener) {
250     myProjectJdkEventDispatcher.removeListener(listener);
251   }
252
253   @Override
254   public void loadState(@NotNull Element element) {
255     for (ProjectExtension extension : ProjectExtension.EP_NAME.getExtensions(myProject)) {
256       extension.readExternal(element);
257     }
258     myProjectSdkName = element.getAttributeValue(PROJECT_JDK_NAME_ATTR);
259     myProjectSdkType = element.getAttributeValue(PROJECT_JDK_TYPE_ATTR);
260   }
261
262   @Override
263   public Element getState() {
264     Element element = new Element("state");
265     element.setAttribute(ATTRIBUTE_VERSION, "2");
266     for (ProjectExtension extension : ProjectExtension.EP_NAME.getExtensions(myProject)) {
267       extension.writeExternal(element);
268     }
269     if (myProjectSdkName != null) {
270       element.setAttribute(PROJECT_JDK_NAME_ATTR, myProjectSdkName);
271     }
272     if (myProjectSdkType != null) {
273       element.setAttribute(PROJECT_JDK_TYPE_ATTR, myProjectSdkType);
274     }
275
276     if (element.getAttributes().size() == 1) {
277       // remove empty element to not write defaults
278       element.removeAttribute(ATTRIBUTE_VERSION);
279     }
280     return element;
281   }
282
283   private boolean myMergedCallStarted;
284   private boolean myMergedCallHasRootChange;
285   private int myRootsChangesDepth;
286
287   @Override
288   public void mergeRootsChangesDuring(@NotNull Runnable runnable) {
289     if (getBatchSession(false).myBatchLevel == 0 && !myMergedCallStarted) {
290       if (myRootsChangesDepth != 0) {
291         int depth = myRootsChangesDepth;
292         myRootsChangesDepth = 0;
293         LOG.error("Merged rootsChanged not allowed inside rootsChanged, rootsChanged level == " + depth);
294       }
295       myMergedCallStarted = true;
296       myMergedCallHasRootChange = false;
297       try {
298         runnable.run();
299       }
300       finally {
301         if (myMergedCallHasRootChange) {
302           LOG.assertTrue(myRootsChangesDepth == 1, "myMergedCallDepth = " + myRootsChangesDepth);
303           getBatchSession(false).rootsChanged();
304         }
305         myMergedCallStarted = false;
306         myMergedCallHasRootChange = false;
307       }
308     }
309     else {
310       runnable.run();
311     }
312   }
313
314   protected void clearScopesCaches() {
315     clearScopesCachesForModules();
316   }
317
318   @Override
319   public void clearScopesCachesForModules() {
320     myRootsCache.clearCache();
321     Module[] modules = ModuleManager.getInstance(myProject).getModules();
322     for (Module module : modules) {
323       ModuleRootManagerEx.getInstanceEx(module).dropCaches();
324     }
325   }
326
327   @Override
328   public void makeRootsChange(@NotNull Runnable runnable, boolean fileTypes, boolean fireEvents) {
329     if (myProject.isDisposed() || Disposer.isDisposing(myProject)) return;
330     BatchSession session = getBatchSession(fileTypes);
331     try {
332       if (fireEvents) session.beforeRootsChanged();
333       runnable.run();
334     }
335     finally {
336       if (fireEvents) session.rootsChanged();
337     }
338   }
339
340   @NotNull
341   protected BatchSession getBatchSession(final boolean fileTypes) {
342     return fileTypes ? myFileTypesChanged : myRootsChanged;
343   }
344
345   protected boolean isFiringEvent;
346
347   private boolean fireBeforeRootsChanged(boolean fileTypes) {
348     ApplicationManager.getApplication().assertWriteAccessAllowed();
349
350     LOG.assertTrue(!isFiringEvent, "Do not use API that changes roots from roots events. Try using invoke later or something else.");
351
352     if (myMergedCallStarted) {
353       LOG.assertTrue(!fileTypes, "File types change is not supported inside merged call");
354     }
355
356     if (myRootsChangesDepth++ == 0) {
357       if (myMergedCallStarted) {
358         myMergedCallHasRootChange = true;
359         myRootsChangesDepth++; // blocks all firing until finishRootsChangedOnDemand
360       }
361       fireBeforeRootsChangeEvent(fileTypes);
362       return true;
363     }
364
365     return false;
366   }
367
368   @ApiStatus.Internal
369   protected void fireBeforeRootsChangeEvent(boolean fileTypes) { }
370
371   private boolean fireRootsChanged(boolean fileTypes) {
372     if (myProject.isDisposed() || Disposer.isDisposing(myProject)) return false;
373
374     ApplicationManager.getApplication().assertWriteAccessAllowed();
375
376     LOG.assertTrue(!isFiringEvent, "Do not use API that changes roots from roots events. Try using invoke later or something else.");
377
378     if (myMergedCallStarted) {
379       LOG.assertTrue(!fileTypes, "File types change is not supported inside merged call");
380     }
381
382     myRootsChangesDepth--;
383     if (myRootsChangesDepth > 0) return false;
384     if (myRootsChangesDepth < 0) {
385       LOG.info("Restoring from roots change start/finish mismatch: ", new Throwable());
386       myRootsChangesDepth = 0;
387     }
388
389     clearScopesCaches();
390
391     incModificationCount();
392
393     fireRootsChangedEvent(fileTypes);
394
395     return true;
396   }
397
398   @ApiStatus.Internal
399   protected void fireRootsChangedEvent(boolean fileTypes) { }
400
401   @NotNull
402   public Project getProject() {
403     return myProject;
404   }
405
406   @NotNull
407   public static String extractLocalPath(@NotNull String url) {
408     String path = VfsUtilCore.urlToPath(url);
409     int separatorIndex = path.indexOf(URLUtil.JAR_SEPARATOR);
410     return separatorIndex > 0 ? path.substring(0, separatorIndex) : path;
411   }
412
413   @NotNull
414   private ModuleManager getModuleManager() {
415     return ModuleManager.getInstance(myProject);
416   }
417
418   void subscribeToRootProvider(@NotNull OrderEntry owner, @NotNull RootProvider provider) {
419     synchronized (myRegisteredRootProviders) {
420       Set<OrderEntry> owners = myRegisteredRootProviders.get(provider);
421       if (owners == null) {
422         owners = new HashSet<>();
423         myRegisteredRootProviders.put(provider, owners);
424         provider.addRootSetChangedListener(myRootProviderChangeListener);
425       }
426       owners.add(owner);
427     }
428   }
429
430   void unsubscribeFromRootProvider(@NotNull OrderEntry owner, @NotNull RootProvider provider) {
431     synchronized (myRegisteredRootProviders) {
432       Set<OrderEntry> owners = myRegisteredRootProviders.get(provider);
433       if (owners != null) {
434         owners.remove(owner);
435         if (owners.isEmpty()) {
436           provider.removeRootSetChangedListener(myRootProviderChangeListener);
437           myRegisteredRootProviders.remove(provider);
438         }
439       }
440     }
441   }
442
443   void addListenerForTable(@NotNull LibraryTable.Listener libraryListener, @NotNull LibraryTable libraryTable) {
444     synchronized (myLibraryTableListenersLock) {
445       LibraryTableMultiListener multiListener = myLibraryTableMultiListeners.get(libraryTable);
446       if (multiListener == null) {
447         multiListener = new LibraryTableMultiListener();
448         libraryTable.addListener(multiListener);
449         myLibraryTableMultiListeners.put(libraryTable, multiListener);
450       }
451       multiListener.addListener(libraryListener);
452     }
453   }
454
455   void removeListenerForTable(@NotNull LibraryTable.Listener libraryListener, @NotNull LibraryTable libraryTable) {
456     synchronized (myLibraryTableListenersLock) {
457       LibraryTableMultiListener multiListener = myLibraryTableMultiListeners.get(libraryTable);
458       if (multiListener != null) {
459         boolean last = multiListener.removeListener(libraryListener);
460         if (last) {
461           libraryTable.removeListener(multiListener);
462           myLibraryTableMultiListeners.remove(libraryTable);
463         }
464       }
465     }
466   }
467
468   private final Object myLibraryTableListenersLock = new Object();
469   private final Map<LibraryTable, LibraryTableMultiListener> myLibraryTableMultiListeners = new HashMap<>();
470
471   private static class ListenerContainer<T> {
472     private final Set<T> myListeners = new LinkedHashSet<>();
473     @NotNull private final T[] myEmptyArray;
474     private T[] myListenersArray;
475
476     private ListenerContainer(@NotNull T[] emptyArray) {
477       myEmptyArray = emptyArray;
478     }
479
480     synchronized void addListener(@NotNull T listener) {
481       myListeners.add(listener);
482       myListenersArray = null;
483     }
484
485     synchronized boolean removeListener(@NotNull T listener) {
486       myListeners.remove(listener);
487       myListenersArray = null;
488       return myListeners.isEmpty();
489     }
490
491     @NotNull
492     synchronized T[] getListeners() {
493       if (myListenersArray == null) {
494         myListenersArray = myListeners.toArray(myEmptyArray);
495       }
496       return myListenersArray;
497     }
498   }
499
500   private class LibraryTableMultiListener extends ListenerContainer<LibraryTable.Listener> implements LibraryTable.Listener {
501     private LibraryTableMultiListener() {
502       super(new LibraryTable.Listener[0]);
503     }
504
505     @Override
506     public void afterLibraryAdded(@NotNull final Library newLibrary) {
507       incModificationCount();
508       mergeRootsChangesDuring(() -> {
509         for (LibraryTable.Listener listener : getListeners()) {
510           listener.afterLibraryAdded(newLibrary);
511         }
512       });
513     }
514
515     @Override
516     public void afterLibraryRenamed(@NotNull final Library library) {
517       incModificationCount();
518       mergeRootsChangesDuring(() -> {
519         for (LibraryTable.Listener listener : getListeners()) {
520           listener.afterLibraryRenamed(library);
521         }
522       });
523     }
524
525     @Override
526     public void beforeLibraryRemoved(@NotNull final Library library) {
527       incModificationCount();
528       mergeRootsChangesDuring(() -> {
529         for (LibraryTable.Listener listener : getListeners()) {
530           listener.beforeLibraryRemoved(library);
531         }
532       });
533     }
534
535     @Override
536     public void afterLibraryRemoved(@NotNull final Library library) {
537       incModificationCount();
538       mergeRootsChangesDuring(() -> {
539         for (LibraryTable.Listener listener : getListeners()) {
540           listener.afterLibraryRemoved(library);
541         }
542       });
543     }
544   }
545
546   private final JdkTableMultiListener myJdkTableMultiListener;
547
548   private final class JdkTableMultiListener extends ListenerContainer<ProjectJdkTable.Listener> implements ProjectJdkTable.Listener {
549     private JdkTableMultiListener(@NotNull Project project) {
550       super(new ProjectJdkTable.Listener[0]);
551
552       project.getMessageBus().connect().subscribe(ProjectJdkTable.JDK_TABLE_TOPIC, this);
553     }
554
555     @Override
556     public void jdkAdded(@NotNull final Sdk jdk) {
557       mergeRootsChangesDuring(() -> {
558         for (ProjectJdkTable.Listener listener : getListeners()) {
559           listener.jdkAdded(jdk);
560         }
561       });
562     }
563
564     @Override
565     public void jdkRemoved(@NotNull final Sdk jdk) {
566       mergeRootsChangesDuring(() -> {
567         for (ProjectJdkTable.Listener listener : getListeners()) {
568           listener.jdkRemoved(jdk);
569         }
570       });
571     }
572
573     @Override
574     public void jdkNameChanged(@NotNull final Sdk jdk, @NotNull final String previousName) {
575       mergeRootsChangesDuring(() -> {
576         for (ProjectJdkTable.Listener listener : getListeners()) {
577           listener.jdkNameChanged(jdk, previousName);
578         }
579       });
580       String currentName = getProjectSdkName();
581       if (previousName.equals(currentName)) {
582         // if already had jdk name and that name was the name of the jdk just changed
583         myProjectSdkName = jdk.getName();
584         myProjectSdkType = jdk.getSdkType().getName();
585       }
586     }
587   }
588
589   private final Map<RootProvider, Set<OrderEntry>> myRegisteredRootProviders = ContainerUtil.newIdentityTroveMap();
590
591   void addJdkTableListener(@NotNull ProjectJdkTable.Listener jdkTableListener, @NotNull Disposable parent) {
592     myJdkTableMultiListener.addListener(jdkTableListener);
593     Disposer.register(parent, () -> myJdkTableMultiListener.removeListener(jdkTableListener));
594   }
595
596   void assertListenersAreDisposed() {
597     synchronized (myRegisteredRootProviders) {
598       if (!myRegisteredRootProviders.isEmpty()) {
599         StringBuilder details = new StringBuilder();
600         for (Map.Entry<RootProvider, Set<OrderEntry>> entry : myRegisteredRootProviders.entrySet()) {
601           details.append(" ").append(entry.getKey()).append(" referenced by ").append(entry.getValue().size()).append(" order entries:\n");
602           for (OrderEntry orderEntry : entry.getValue()) {
603             details.append("   ").append(orderEntry).append("\n");
604           }
605         }
606         LOG.error("Listeners for " + myRegisteredRootProviders.size() + " root providers aren't disposed:" + details);
607         for (RootProvider provider : myRegisteredRootProviders.keySet()) {
608           provider.removeRootSetChangedListener(myRootProviderChangeListener);
609         }
610       }
611     }
612   }
613
614   private class RootProviderChangeListener implements RootProvider.RootSetChangedListener {
615     private boolean myInsideRootsChange;
616
617     @Override
618     public void rootSetChanged(@NotNull final RootProvider wrapper) {
619       if (myInsideRootsChange) return;
620       myInsideRootsChange = true;
621       try {
622         makeRootsChange(EmptyRunnable.INSTANCE, false, true);
623       }
624       finally {
625         myInsideRootsChange = false;
626       }
627     }
628   }
629
630   @Override
631   public void markRootsForRefresh() {
632
633   }
634
635   @NotNull
636   public VirtualFilePointerListener getRootsValidityChangedListener() {
637     return myRootsValidityChangedListener;
638   }
639 }