| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.intellij.openapi.roots.impl; |
| |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.components.ProjectComponent; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.extensions.Extensions; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.module.ModuleManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.projectRoots.ProjectJdkTable; |
| import com.intellij.openapi.projectRoots.Sdk; |
| import com.intellij.openapi.roots.*; |
| import com.intellij.openapi.roots.ex.ProjectRootManagerEx; |
| import com.intellij.openapi.roots.libraries.Library; |
| import com.intellij.openapi.roots.libraries.LibraryTable; |
| import com.intellij.openapi.util.EmptyRunnable; |
| import com.intellij.openapi.util.InvalidDataException; |
| import com.intellij.openapi.util.JDOMExternalizable; |
| import com.intellij.openapi.util.WriteExternalException; |
| import com.intellij.openapi.vfs.VfsUtilCore; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiManager; |
| import com.intellij.psi.impl.PsiModificationTrackerImpl; |
| import com.intellij.util.EventDispatcher; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.HashMap; |
| import com.intellij.util.containers.HashSet; |
| import com.intellij.util.io.URLUtil; |
| import com.intellij.util.messages.MessageBusConnection; |
| import org.jdom.Element; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.jps.model.module.JpsModuleSourceRootType; |
| |
| import java.util.*; |
| |
| /** |
| * @author max |
| */ |
| public class ProjectRootManagerImpl extends ProjectRootManagerEx implements ProjectComponent, JDOMExternalizable { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.projectRoots.impl.ProjectRootManagerImpl"); |
| |
| @NonNls public static final String PROJECT_JDK_NAME_ATTR = "project-jdk-name"; |
| @NonNls public static final String PROJECT_JDK_TYPE_ATTR = "project-jdk-type"; |
| |
| protected final Project myProject; |
| |
| private final EventDispatcher<ProjectJdkListener> myProjectJdkEventDispatcher = EventDispatcher.create(ProjectJdkListener.class); |
| |
| private String myProjectSdkName; |
| private String myProjectSdkType; |
| |
| @NonNls private static final String ATTRIBUTE_VERSION = "version"; |
| |
| private final OrderRootsCache myRootsCache; |
| |
| protected boolean myStartupActivityPerformed = false; |
| |
| private final RootProviderChangeListener myRootProviderChangeListener = new RootProviderChangeListener(); |
| |
| protected class BatchSession { |
| private int myBatchLevel = 0; |
| private boolean myChanged = false; |
| |
| private final boolean myFileTypes; |
| |
| private BatchSession(final boolean fileTypes) { |
| myFileTypes = fileTypes; |
| } |
| |
| protected void levelUp() { |
| if (myBatchLevel == 0) { |
| myChanged = false; |
| } |
| myBatchLevel += 1; |
| } |
| |
| protected void levelDown() { |
| myBatchLevel -= 1; |
| if (myChanged && myBatchLevel == 0) { |
| try { |
| fireChange(); |
| } |
| finally { |
| myChanged = false; |
| } |
| } |
| } |
| |
| private boolean fireChange() { |
| return fireRootsChanged(myFileTypes); |
| } |
| |
| protected void beforeRootsChanged() { |
| if (myBatchLevel == 0 || !myChanged) { |
| if (fireBeforeRootsChanged(myFileTypes)) { |
| myChanged = true; |
| } |
| } |
| } |
| |
| protected void rootsChanged() { |
| if (myBatchLevel == 0) { |
| if (fireChange()) { |
| myChanged = false; |
| } |
| } |
| } |
| } |
| |
| protected final BatchSession myRootsChanged = new BatchSession(false); |
| protected final BatchSession myFileTypesChanged = new BatchSession(true); |
| |
| public static ProjectRootManagerImpl getInstanceImpl(Project project) { |
| return (ProjectRootManagerImpl)getInstance(project); |
| } |
| |
| public ProjectRootManagerImpl(Project project) { |
| myProject = project; |
| myRootsCache = new OrderRootsCache(project); |
| myJdkTableMultiListener = new JdkTableMultiListener(project); |
| } |
| |
| @Override |
| @NotNull |
| public ProjectFileIndex getFileIndex() { |
| return ProjectFileIndex.SERVICE.getInstance(myProject); |
| } |
| |
| @Override |
| @NotNull |
| public List<String> getContentRootUrls() { |
| final List<String> result = new ArrayList<String>(); |
| for (Module module : getModuleManager().getModules()) { |
| final String[] urls = ModuleRootManager.getInstance(module).getContentRootUrls(); |
| ContainerUtil.addAll(result, urls); |
| } |
| return result; |
| } |
| |
| @Override |
| @NotNull |
| public VirtualFile[] getContentRoots() { |
| final List<VirtualFile> result = new ArrayList<VirtualFile>(); |
| for (Module module : getModuleManager().getModules()) { |
| final VirtualFile[] contentRoots = ModuleRootManager.getInstance(module).getContentRoots(); |
| ContainerUtil.addAll(result, contentRoots); |
| } |
| return VfsUtilCore.toVirtualFileArray(result); |
| } |
| |
| @NotNull |
| @Override |
| public VirtualFile[] getContentSourceRoots() { |
| final List<VirtualFile> result = new ArrayList<VirtualFile>(); |
| for (Module module : getModuleManager().getModules()) { |
| final VirtualFile[] sourceRoots = ModuleRootManager.getInstance(module).getSourceRoots(); |
| ContainerUtil.addAll(result, sourceRoots); |
| } |
| return VfsUtilCore.toVirtualFileArray(result); |
| } |
| |
| @NotNull |
| @Override |
| public List<VirtualFile> getModuleSourceRoots(@NotNull Set<? extends JpsModuleSourceRootType<?>> rootTypes) { |
| List<VirtualFile> roots = new ArrayList<VirtualFile>(); |
| for (Module module : getModuleManager().getModules()) { |
| roots.addAll(ModuleRootManager.getInstance(module).getSourceRoots(rootTypes)); |
| } |
| return roots; |
| } |
| |
| @NotNull |
| @Override |
| public OrderEnumerator orderEntries() { |
| return new ProjectOrderEnumerator(myProject, myRootsCache); |
| } |
| |
| @NotNull |
| @Override |
| public OrderEnumerator orderEntries(@NotNull Collection<? extends Module> modules) { |
| return new ModulesOrderEnumerator(myProject, modules); |
| } |
| |
| @Override |
| public VirtualFile[] getContentRootsFromAllModules() { |
| List<VirtualFile> result = new ArrayList<VirtualFile>(); |
| final Module[] modules = getModuleManager().getSortedModules(); |
| for (Module module : modules) { |
| final VirtualFile[] files = ModuleRootManager.getInstance(module).getContentRoots(); |
| ContainerUtil.addAll(result, files); |
| } |
| result.add(myProject.getBaseDir()); |
| return VfsUtilCore.toVirtualFileArray(result); |
| } |
| |
| @Override |
| public Sdk getProjectSdk() { |
| return myProjectSdkName == null ? null : ProjectJdkTable.getInstance().findJdk(myProjectSdkName, myProjectSdkType); |
| } |
| |
| @Override |
| public String getProjectSdkName() { |
| return myProjectSdkName; |
| } |
| |
| @Override |
| public void setProjectSdk(Sdk sdk) { |
| ApplicationManager.getApplication().assertWriteAccessAllowed(); |
| if (sdk == null) { |
| myProjectSdkName = null; |
| myProjectSdkType = null; |
| } |
| else { |
| myProjectSdkName = sdk.getName(); |
| myProjectSdkType = sdk.getSdkType().getName(); |
| } |
| mergeRootsChangesDuring(new Runnable() { |
| @Override |
| public void run() { |
| myProjectJdkEventDispatcher.getMulticaster().projectJdkChanged(); |
| } |
| }); |
| } |
| |
| @Override |
| public void setProjectSdkName(String name) { |
| ApplicationManager.getApplication().assertWriteAccessAllowed(); |
| myProjectSdkName = name; |
| |
| mergeRootsChangesDuring(new Runnable() { |
| @Override |
| public void run() { |
| myProjectJdkEventDispatcher.getMulticaster().projectJdkChanged(); |
| } |
| }); |
| } |
| |
| @Override |
| public void addProjectJdkListener(ProjectJdkListener listener) { |
| myProjectJdkEventDispatcher.addListener(listener); |
| } |
| |
| @Override |
| public void removeProjectJdkListener(ProjectJdkListener listener) { |
| myProjectJdkEventDispatcher.removeListener(listener); |
| } |
| |
| @Override |
| public void projectOpened() { |
| } |
| |
| @Override |
| public void projectClosed() { |
| } |
| |
| @Override |
| @NotNull |
| public String getComponentName() { |
| return "ProjectRootManager"; |
| } |
| |
| @Override |
| public void initComponent() { |
| } |
| |
| @Override |
| public void disposeComponent() { |
| } |
| |
| @Override |
| public void readExternal(Element element) throws InvalidDataException { |
| for (ProjectExtension extension : Extensions.getExtensions(ProjectExtension.EP_NAME, myProject)) { |
| extension.readExternal(element); |
| } |
| myProjectSdkName = element.getAttributeValue(PROJECT_JDK_NAME_ATTR); |
| myProjectSdkType = element.getAttributeValue(PROJECT_JDK_TYPE_ATTR); |
| } |
| |
| @Override |
| public void writeExternal(Element element) throws WriteExternalException { |
| element.setAttribute(ATTRIBUTE_VERSION, "2"); |
| for (ProjectExtension extension : Extensions.getExtensions(ProjectExtension.EP_NAME, myProject)) { |
| extension.writeExternal(element); |
| } |
| if (myProjectSdkName != null) { |
| element.setAttribute(PROJECT_JDK_NAME_ATTR, myProjectSdkName); |
| } |
| if (myProjectSdkType != null) { |
| element.setAttribute(PROJECT_JDK_TYPE_ATTR, myProjectSdkType); |
| } |
| } |
| |
| private boolean myMergedCallStarted = false; |
| private boolean myMergedCallHasRootChange = false; |
| private int myRootsChangesDepth = 0; |
| |
| @Override |
| public void mergeRootsChangesDuring(@NotNull Runnable runnable) { |
| if (getBatchSession(false).myBatchLevel == 0 && !myMergedCallStarted) { |
| if (myRootsChangesDepth != 0) { |
| int depth = myRootsChangesDepth; |
| myRootsChangesDepth = 0; |
| LOG.error("Merged rootsChanged not allowed inside rootsChanged, rootsChanged level == " + depth); |
| } |
| myMergedCallStarted = true; |
| myMergedCallHasRootChange = false; |
| try { |
| runnable.run(); |
| } |
| finally { |
| if (myMergedCallHasRootChange) { |
| LOG.assertTrue(myRootsChangesDepth == 1, "myMergedCallDepth = " + myRootsChangesDepth); |
| getBatchSession(false).rootsChanged(); |
| } |
| myMergedCallStarted = false; |
| myMergedCallHasRootChange = false; |
| } |
| } |
| else { |
| runnable.run(); |
| } |
| } |
| |
| protected void clearScopesCaches() { |
| clearScopesCachesForModules(); |
| } |
| |
| @Override |
| public void clearScopesCachesForModules() { |
| myRootsCache.clearCache(); |
| Module[] modules = ModuleManager.getInstance(myProject).getModules(); |
| for (Module module : modules) { |
| ((ModuleRootManagerImpl)ModuleRootManager.getInstance(module)).dropCaches(); |
| } |
| } |
| |
| @Override |
| public void makeRootsChange(@NotNull Runnable runnable, boolean fileTypes, boolean fireEvents) { |
| if (myProject.isDisposed()) return; |
| BatchSession session = getBatchSession(fileTypes); |
| if (fireEvents) session.beforeRootsChanged(); |
| try { |
| runnable.run(); |
| } |
| finally { |
| if (fireEvents) session.rootsChanged(); |
| } |
| } |
| |
| protected BatchSession getBatchSession(final boolean fileTypes) { |
| return fileTypes ? myFileTypesChanged : myRootsChanged; |
| } |
| |
| protected boolean isFiringEvent = false; |
| |
| private boolean fireBeforeRootsChanged(boolean fileTypes) { |
| ApplicationManager.getApplication().assertWriteAccessAllowed(); |
| |
| LOG.assertTrue(!isFiringEvent, "Do not use API that changes roots from roots events. Try using invoke later or something else."); |
| |
| if (myMergedCallStarted) { |
| LOG.assertTrue(!fileTypes, "File types change is not supported inside merged call"); |
| } |
| |
| if (myRootsChangesDepth++ == 0) { |
| if (myMergedCallStarted) { |
| myMergedCallHasRootChange = true; |
| myRootsChangesDepth++; // blocks all firing until finishRootsChangedOnDemand |
| } |
| fireBeforeRootsChangeEvent(fileTypes); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| protected void fireBeforeRootsChangeEvent(boolean fileTypes) { |
| } |
| |
| private boolean fireRootsChanged(boolean fileTypes) { |
| if (myProject.isDisposed()) return false; |
| |
| ApplicationManager.getApplication().assertWriteAccessAllowed(); |
| |
| LOG.assertTrue(!isFiringEvent, "Do not use API that changes roots from roots events. Try using invoke later or something else."); |
| |
| if (myMergedCallStarted) { |
| LOG.assertTrue(!fileTypes, "File types change is not supported inside merged call"); |
| } |
| |
| myRootsChangesDepth--; |
| if (myRootsChangesDepth > 0) return false; |
| if (myRootsChangesDepth < 0) { |
| LOG.info("Restoring from roots change start/finish mismatch: ", new Throwable()); |
| myRootsChangesDepth = 0; |
| } |
| |
| clearScopesCaches(); |
| |
| incModificationCount(); |
| |
| PsiManager psiManager = PsiManager.getInstance(myProject); |
| psiManager.dropResolveCaches(); |
| ((PsiModificationTrackerImpl)psiManager.getModificationTracker()).incCounter(); |
| |
| fireRootsChangedEvent(fileTypes); |
| |
| doSynchronizeRoots(); |
| |
| addRootsToWatch(); |
| |
| return true; |
| } |
| |
| protected void fireRootsChangedEvent(boolean fileTypes) { |
| } |
| |
| protected void addRootsToWatch() { |
| } |
| |
| public Project getProject() { |
| return myProject; |
| } |
| |
| protected void doSynchronizeRoots() { |
| } |
| |
| public static String extractLocalPath(final String url) { |
| final String path = VfsUtilCore.urlToPath(url); |
| final int jarSeparatorIndex = path.indexOf(URLUtil.JAR_SEPARATOR); |
| if (jarSeparatorIndex > 0) { |
| return path.substring(0, jarSeparatorIndex); |
| } |
| return path; |
| } |
| |
| private ModuleManager getModuleManager() { |
| return ModuleManager.getInstance(myProject); |
| } |
| |
| void subscribeToRootProvider(OrderEntry owner, final RootProvider provider) { |
| Set<OrderEntry> owners = myRegisteredRootProviders.get(provider); |
| if (owners == null) { |
| owners = new HashSet<OrderEntry>(); |
| myRegisteredRootProviders.put(provider, owners); |
| provider.addRootSetChangedListener(myRootProviderChangeListener); |
| } |
| owners.add(owner); |
| } |
| |
| void unsubscribeFromRootProvider(OrderEntry owner, final RootProvider provider) { |
| Set<OrderEntry> owners = myRegisteredRootProviders.get(provider); |
| if (owners != null) { |
| owners.remove(owner); |
| if (owners.isEmpty()) { |
| provider.removeRootSetChangedListener(myRootProviderChangeListener); |
| myRegisteredRootProviders.remove(provider); |
| } |
| } |
| } |
| |
| void addListenerForTable(LibraryTable.Listener libraryListener, |
| final LibraryTable libraryTable) { |
| synchronized (myLibraryTableListenersLock) { |
| LibraryTableMultiListener multiListener = myLibraryTableMultiListeners.get(libraryTable); |
| if (multiListener == null) { |
| multiListener = new LibraryTableMultiListener(libraryTable); |
| libraryTable.addListener(multiListener); |
| myLibraryTableMultiListeners.put(libraryTable, multiListener); |
| } |
| multiListener.addListener(libraryListener); |
| } |
| } |
| |
| void removeListenerForTable(LibraryTable.Listener libraryListener, |
| final LibraryTable libraryTable) { |
| synchronized (myLibraryTableListenersLock) { |
| LibraryTableMultiListener multiListener = myLibraryTableMultiListeners.get(libraryTable); |
| if (multiListener != null) { |
| boolean last = multiListener.removeListener(libraryListener); |
| if (last) { |
| libraryTable.removeListener(multiListener); |
| myLibraryTableMultiListeners.remove(libraryTable); |
| } |
| } |
| } |
| } |
| |
| private final Object myLibraryTableListenersLock = new Object(); |
| private final Map<LibraryTable, LibraryTableMultiListener> myLibraryTableMultiListeners = new HashMap<LibraryTable, LibraryTableMultiListener>(); |
| |
| private class LibraryTableMultiListener implements LibraryTable.Listener { |
| private final Set<LibraryTable.Listener> myListeners = new LinkedHashSet<LibraryTable.Listener>(); |
| private final LibraryTable myLibraryTable; |
| private LibraryTable.Listener[] myListenersArray; |
| |
| private LibraryTableMultiListener(LibraryTable libraryTable) { |
| myLibraryTable = libraryTable; |
| } |
| |
| private synchronized void addListener(LibraryTable.Listener listener) { |
| myListeners.add(listener); |
| myListenersArray = null; |
| } |
| |
| private synchronized boolean removeListener(LibraryTable.Listener listener) { |
| myListeners.remove(listener); |
| myListenersArray = null; |
| return myListeners.isEmpty(); |
| } |
| |
| @Override |
| public void afterLibraryAdded(final Library newLibrary) { |
| incModificationCount(); |
| mergeRootsChangesDuring(new Runnable() { |
| @Override |
| public void run() { |
| for (LibraryTable.Listener listener : getListeners()) { |
| listener.afterLibraryAdded(newLibrary); |
| } |
| } |
| }); |
| } |
| |
| private synchronized LibraryTable.Listener[] getListeners() { |
| if (myListenersArray == null) { |
| myListenersArray = myListeners.toArray(new LibraryTable.Listener[myListeners.size()]); |
| } |
| return myListenersArray; |
| } |
| |
| @Override |
| public void afterLibraryRenamed(final Library library) { |
| incModificationCount(); |
| mergeRootsChangesDuring(new Runnable() { |
| @Override |
| public void run() { |
| for (LibraryTable.Listener listener : getListeners()) { |
| listener.afterLibraryRenamed(library); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void beforeLibraryRemoved(final Library library) { |
| incModificationCount(); |
| mergeRootsChangesDuring(new Runnable() { |
| @Override |
| public void run() { |
| for (LibraryTable.Listener listener : getListeners()) { |
| listener.beforeLibraryRemoved(library); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void afterLibraryRemoved(final Library library) { |
| incModificationCount(); |
| mergeRootsChangesDuring(new Runnable() { |
| @Override |
| public void run() { |
| for (LibraryTable.Listener listener : getListeners()) { |
| listener.afterLibraryRemoved(library); |
| } |
| } |
| }); |
| } |
| } |
| |
| private final JdkTableMultiListener myJdkTableMultiListener; |
| |
| private class JdkTableMultiListener implements ProjectJdkTable.Listener { |
| private final Set<ProjectJdkTable.Listener> myListeners = new LinkedHashSet<ProjectJdkTable.Listener>(); |
| private MessageBusConnection listenerConnection; |
| private ProjectJdkTable.Listener[] myListenersArray; |
| |
| private JdkTableMultiListener(Project project) { |
| listenerConnection = project.getMessageBus().connect(); |
| listenerConnection.subscribe(ProjectJdkTable.JDK_TABLE_TOPIC, this); |
| } |
| |
| private synchronized void addListener(ProjectJdkTable.Listener listener) { |
| myListeners.add(listener); |
| myListenersArray = null; |
| } |
| |
| private synchronized void removeListener(ProjectJdkTable.Listener listener) { |
| myListeners.remove(listener); |
| myListenersArray = null; |
| } |
| |
| private synchronized ProjectJdkTable.Listener[] getListeners() { |
| if (myListenersArray == null) { |
| myListenersArray = myListeners.toArray(new ProjectJdkTable.Listener[myListeners.size()]); |
| } |
| return myListenersArray; |
| } |
| |
| @Override |
| public void jdkAdded(final Sdk jdk) { |
| mergeRootsChangesDuring(new Runnable() { |
| @Override |
| public void run() { |
| for (ProjectJdkTable.Listener listener : getListeners()) { |
| listener.jdkAdded(jdk); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void jdkRemoved(final Sdk jdk) { |
| mergeRootsChangesDuring(new Runnable() { |
| @Override |
| public void run() { |
| for (ProjectJdkTable.Listener listener : getListeners()) { |
| listener.jdkRemoved(jdk); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void jdkNameChanged(final Sdk jdk, final String previousName) { |
| mergeRootsChangesDuring(new Runnable() { |
| @Override |
| public void run() { |
| for (ProjectJdkTable.Listener listener : getListeners()) { |
| listener.jdkNameChanged(jdk, previousName); |
| } |
| } |
| }); |
| String currentName = getProjectSdkName(); |
| if (previousName != null && previousName.equals(currentName)) { |
| // if already had jdk name and that name was the name of the jdk just changed |
| myProjectSdkName = jdk.getName(); |
| myProjectSdkType = jdk.getSdkType().getName(); |
| } |
| } |
| } |
| |
| private final Map<RootProvider, Set<OrderEntry>> myRegisteredRootProviders = new HashMap<RootProvider, Set<OrderEntry>>(); |
| |
| void addJdkTableListener(ProjectJdkTable.Listener jdkTableListener) { |
| myJdkTableMultiListener.addListener(jdkTableListener); |
| } |
| |
| void removeJdkTableListener(ProjectJdkTable.Listener jdkTableListener) { |
| myJdkTableMultiListener.removeListener(jdkTableListener); |
| } |
| |
| private class RootProviderChangeListener implements RootProvider.RootSetChangedListener { |
| private boolean myInsideRootsChange; |
| |
| @Override |
| public void rootSetChanged(final RootProvider wrapper) { |
| if (myInsideRootsChange) return; |
| myInsideRootsChange = true; |
| try { |
| makeRootsChange(EmptyRunnable.INSTANCE, false, true); |
| } |
| finally { |
| myInsideRootsChange = false; |
| } |
| } |
| } |
| } |