/*
* Copyright 2000-2009 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 org.community.intellij.plugins.communitycase.vfs;
import com.intellij.ProjectTopics;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandAdapter;
import com.intellij.openapi.command.CommandEvent;
import com.intellij.openapi.command.CommandListener;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.roots.ModuleRootListener;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vcs.VcsDirectoryMapping;
import com.intellij.openapi.vcs.VcsListener;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.ex.VirtualFileManagerAdapter;
import com.intellij.openapi.vfs.ex.VirtualFileManagerEx;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import org.community.intellij.plugins.communitycase.Util;
import org.community.intellij.plugins.communitycase.Vcs;
import org.community.intellij.plugins.communitycase.i18n.Bundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.event.HyperlinkEvent;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* The component tracks roots for the project. If roots are mapped incorrectly it
* shows balloon that notifies user about the problem and offers to correct root mapping.
*/
public class RootTracker implements VcsListener {
/**
* The context project
*/
private final Project myProject;
/**
* Tracker of roots for project root manager
*/
private final ProjectRootManager myProjectRoots;
/**
* The vcs manager that tracks content roots
*/
private final ProjectLevelVcsManager myVcsManager;
/**
* The vcs instance
*/
private final Vcs myVcs;
/**
* If true, the tracking is enabled.
*/
private final AtomicBoolean myIsEnabled = new AtomicBoolean(false);
/**
* If true, the root configuration has been possibly invalidated
*/
private final AtomicBoolean myRootsInvalidated = new AtomicBoolean(true);
/**
* If true, there are some configured roots, or listener has never been run yet
*/
private final AtomicBoolean myHasRoots = new AtomicBoolean(true);
/**
* If true, the notification is currently active and has not been dismissed yet.
*/
private final AtomicBoolean myNotificationPosted = new AtomicBoolean(false);
private final MergingUpdateQueue myQueue;
private Notification myNotification;
/**
* The invalid roots
*/
private static final String _INVALID_ROOTS_ID = "";
/**
* The command listener
*/
private final CommandListener myCommandListener;
/**
* The file listener
*/
private final MyFileListener myFileListener;
/**
* Listener for refresh events
*/
private final VirtualFileManagerAdapter myVirtualFileManagerListener;
/**
* Local file system service
*/
private final LocalFileSystem myLocalFileSystem;
/**
* The multicaster for root events
*/
private final RootsListener myMulticaster;
private final MessageBusConnection myMessageBusConnection;
/**
* The constructor
*
* @param project the project instance
* @param multicaster the listeners to notify
*/
public RootTracker(Vcs vcs, @NotNull Project project, @NotNull RootsListener multicaster) {
myMulticaster = multicaster;
if (project.isDefault()) {
throw new IllegalArgumentException("The project must not be default");
}
myProject = project;
myProjectRoots = ProjectRootManager.getInstance(myProject);
myQueue = new MergingUpdateQueue("queue", 500, true, null, project, null, false);
myVcs = vcs;
myVcsManager = ProjectLevelVcsManager.getInstance(project);
myVcsManager.addVcsListener(this);
myLocalFileSystem = LocalFileSystem.getInstance();
myMessageBusConnection = myProject.getMessageBus().connect();
myMessageBusConnection.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
public void beforeRootsChange(ModuleRootEvent event) {
// do nothing
}
public void rootsChanged(ModuleRootEvent event) {
invalidate();
}
});
myCommandListener = new CommandAdapter() {
@Override
public void commandFinished(CommandEvent event) {
if (!myRootsInvalidated.compareAndSet(true, false)) {
return;
}
scheduleRootsCheck(false);
}
};
CommandProcessor.getInstance().addCommandListener(myCommandListener);
myFileListener = new MyFileListener();
VirtualFileManagerEx fileManager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
fileManager.addVirtualFileListener(myFileListener);
myVirtualFileManagerListener = new VirtualFileManagerAdapter() {
@Override
public void afterRefreshFinish(boolean asynchonous) {
if (!myRootsInvalidated.compareAndSet(true, false)) {
return;
}
scheduleRootsCheck(false);
}
};
fileManager.addVirtualFileManagerListener(myVirtualFileManagerListener);
StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new Runnable() {
public void run() {
myIsEnabled.set(true);
scheduleRootsCheck(true);
}
});
}
/**
* Dispose the component removing all related listeners
*/
public void dispose() {
myVcsManager.removeVcsListener(this);
myMessageBusConnection.disconnect();
CommandProcessor.getInstance().removeCommandListener(myCommandListener);
VirtualFileManagerEx fileManager = (VirtualFileManagerEx)VirtualFileManager.getInstance();
fileManager.removeVirtualFileListener(myFileListener);
fileManager.removeVirtualFileManagerListener(myVirtualFileManagerListener);
}
/**
* {@inheritDoc}
*/
public void directoryMappingChanged() {
ApplicationManager.getApplication().invokeLater(new Runnable() {
public void run() {
scheduleRootsCheck(true);
}
});
}
private void scheduleRootsCheck(final boolean rootsChanged) {
if (ApplicationManager.getApplication().isUnitTestMode() || ApplicationManager.getApplication().isHeadlessEnvironment()) {
doCheckRoots(rootsChanged);
return;
}
myQueue.queue(new Update("root check") {
public void run() {
if (myProject.isDisposed()) return;
doCheckRoots(rootsChanged);
}
});
}
/**
* Check roots for changes.
*
* @param rootsChanged
*/
private void doCheckRoots(boolean rootsChanged) {
if (!myIsEnabled.get() || (!rootsChanged && !myHasRoots.get())) {
return;
}
final HashSet<VirtualFile> rootSet = new HashSet<VirtualFile>();
boolean hasInvalidRoots = ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
public Boolean compute() {
for (VcsDirectoryMapping m : myVcsManager.getDirectoryMappings()) {
if (!m.getVcs().equals(myVcs.getName())) {
continue;
}
String path = m.getDirectory();
if (path.length() == 0) {
VirtualFile baseDir = myProject.getBaseDir();
assert baseDir != null;
path = baseDir.getPath();
}
VirtualFile root = lookupFile(path);
VirtualFile actual = Util.rootOrNull(root);
if (root == null || rootSet.contains(root) || actual != root) {
return true;
}
rootSet.add(root);
}
return false;
}
});
if (!hasInvalidRoots && rootSet.isEmpty()) {
myHasRoots.set(false);
return;
}
else {
myHasRoots.set(true);
}
if (!hasInvalidRoots) {
// check if roots have a problem
for (final VirtualFile root : rootSet) {
hasInvalidRoots = hasUnmappedSubroots(root, rootSet, 0);
if (hasInvalidRoots) {
break;
}
}
}
if (!hasInvalidRoots) {
// all roots are correct
if (myNotificationPosted.compareAndSet(true, false)) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
if (myNotification != null) {
if (!myNotification.isExpired()) {
myNotification.expire();
}
myNotification = null;
}
}
});
}
}
else if (myNotificationPosted.compareAndSet(false, true)) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
myNotification = new Notification(_INVALID_ROOTS_ID, Bundle.getString("root.tracker.message.title"),
Bundle.getString("root.tracker.message"), NotificationType.ERROR,
new NotificationListener() {
public void hyperlinkUpdate(@NotNull Notification notification,
@NotNull HyperlinkEvent event) {
if (fixRoots()) {
notification.expire();
}
}
});
Notifications.Bus.notify(myNotification, myProject);
}
});
}
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
myMulticaster.RootsChanged();
}
});
}
/**
* Check if there are some unmapped subdirectories under
*
* @param directory the content root to check
* @param rootSet the mapped root set
* @param depth
*/
private static boolean hasUnmappedSubroots(final VirtualFile directory, final @NotNull HashSet<VirtualFile> rootSet, int depth) {
if (depth > 3) { // three is quite enough
return false;
}
VirtualFile[] children = ApplicationManager.getApplication().runReadAction(new Computable<VirtualFile[]>() {
public VirtualFile[] compute() {
return directory.isValid() ? directory.getChildren() : VirtualFile.EMPTY_ARRAY;
}
});
for (final VirtualFile child : children) {
if (!child.isDirectory()) {
continue;
}
if (child.getName().equals(".")) {
return !rootSet.contains(child.getParent());
}
if (hasUnmappedSubroots(child, rootSet, depth + 1)) {
return true;
}
}
return false;
}
/**
* Fix mapped roots
*
* @return true if roots now in the correct state
*/
boolean fixRoots() {
final List<VcsDirectoryMapping> vcsDirectoryMappings = new ArrayList<VcsDirectoryMapping>(myVcsManager.getDirectoryMappings());
final HashSet<String> mapped = new HashSet<String>();
final HashSet<String> removed = new HashSet<String>();
final HashSet<String> added = new HashSet<String>();
final VirtualFile baseDir = myProject.getBaseDir();
ApplicationManager.getApplication().runReadAction(new Runnable() {
public void run() {
for (Iterator<VcsDirectoryMapping> i = vcsDirectoryMappings.iterator(); i.hasNext();) {
VcsDirectoryMapping m = i.next();
String vcsName = myVcs.getName();
if (!vcsName.equals(m.getVcs())) {
continue;
}
String path = m.getDirectory();
if (path.length() == 0 && baseDir != null) {
path = baseDir.getPath();
}
VirtualFile file = lookupFile(path);
if (file != null && !mapped.add(file.getPath())) {
// eliminate duplicates
i.remove();
continue;
}
final VirtualFile actual = Util.rootOrNull(file);
if (file == null || actual == null) {
removed.add(path);
}
else if (actual != file) {
removed.add(path);
added.add(actual.getPath());
}
}
for (String m : mapped) {
VirtualFile file = lookupFile(m);
if (file == null) {
continue;
}
addSubroots(file, added, mapped);
if (removed.contains(m)) {
continue;
}
VirtualFile root = Util.rootOrNull(file);
assert root != null;
for (String o : mapped) {
// the mapped collection is not modified here, so order is being kept
if (o.equals(m) || removed.contains(o)) {
continue;
}
if (o.startsWith(m)) {
VirtualFile otherFile = lookupFile(m);
assert otherFile != null;
VirtualFile otherRoot = Util.rootOrNull(otherFile);
assert otherRoot != null;
if (otherRoot == root) {
removed.add(o);
}
else if (otherFile != otherRoot) {
added.add(otherRoot.getPath());
removed.add(o);
}
}
}
}
}
});
if (added.isEmpty() && removed.isEmpty()) {
Messages.showInfoMessage(myProject, Bundle.message("fix.roots.valid.message"), Bundle.message("fix.roots.valid.title"));
return true;
}
FixRootsDialog d = new FixRootsDialog(myProject, mapped, added, removed);
d.show();
if (!d.isOK()) {
return false;
}
for (Iterator<VcsDirectoryMapping> i = vcsDirectoryMappings.iterator(); i.hasNext();) {
VcsDirectoryMapping m = i.next();
String path = m.getDirectory();
if (removed.contains(path) || (path.length() == 0 && baseDir != null && removed.contains(baseDir.getPath()))) {
i.remove();
}
}
for (String a : added) {
vcsDirectoryMappings.add(new VcsDirectoryMapping(a, myVcs.getName()));
}
myVcsManager.setDirectoryMappings(vcsDirectoryMappings);
myVcsManager.updateActiveVcss();
return true;
}
/**
* Look up file in the file system
*
* @param path the path to lookup
* @return the file or null if the file not found
*/
@Nullable
private VirtualFile lookupFile(String path) {
return myLocalFileSystem.findFileByPath(path);
}
/**
* Add subroots for the content root
*
* @param directory the content root to check
* @param toAdd collection of roots to be added
* @param mapped all mapped roots
*/
private static void addSubroots(VirtualFile directory, HashSet<String> toAdd, HashSet<String> mapped) {
for (VirtualFile child : directory.getChildren()) {
if (!child.isDirectory()) {
continue;
}
if (child.getName().equals(".") && !mapped.contains(directory.getPath())) {
toAdd.add(directory.getPath());
}
else {
addSubroots(child, toAdd, mapped);
}
}
}
/**
* Invalidate root
*/
private void invalidate() {
myRootsInvalidated.set(true);
}
/**
* The listener for roots
*/
private class MyFileListener extends VirtualFileAdapter {
/**
* Return true if file has repositories
*
* @param file the file to check
* @return true if file has repositories
*/
private boolean hasRepositories(VirtualFile file) {
if (!file.isDirectory() || !file.getName().equals(".")) {
return false;
}
VirtualFile baseDir = myProject.getBaseDir();
if (baseDir == null) {
return false;
}
if (!VfsUtil.isAncestor(baseDir, file, false)) {
boolean isUnder = false;
for (VirtualFile c : myProjectRoots.getContentRoots()) {
if (!VfsUtil.isAncestor(baseDir, c, false) && VfsUtil.isAncestor(c, file, false)) {
isUnder = true;
break;
}
}
if (!isUnder) {
return false;
}
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public void fileCreated(VirtualFileEvent event) {
if (!myHasRoots.get()) {
return;
}
if (hasRepositories(event.getFile())) {
invalidate();
}
}
/**
* {@inheritDoc}
*/
@Override
public void beforeFileDeletion(VirtualFileEvent event) {
if (!myHasRoots.get()) {
return;
}
if (hasRepositories(event.getFile())) {
invalidate();
}
}
/**
* {@inheritDoc}
*/
@Override
public void fileMoved(VirtualFileMoveEvent event) {
if (!myHasRoots.get()) {
return;
}
if (hasRepositories(event.getFile())) {
invalidate();
}
}
/**
* {@inheritDoc}
*/
@Override
public void fileCopied(VirtualFileCopyEvent event) {
if (!myHasRoots.get()) {
return;
}
if (hasRepositories(event.getFile())) {
invalidate();
}
}
}
}