blob: b5243e7c7ec99dd6614689d292d75694f2a93451 [file] [log] [blame]
/*
* 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.project.impl;
import com.intellij.CommonBundle;
import com.intellij.conversion.ConversionResult;
import com.intellij.conversion.ConversionService;
import com.intellij.ide.AppLifecycleListener;
import com.intellij.ide.RecentProjectsManagerBase;
import com.intellij.ide.impl.ProjectUtil;
import com.intellij.ide.plugins.PluginManager;
import com.intellij.ide.startup.impl.StartupManagerImpl;
import com.intellij.notification.NotificationsManager;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.*;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.application.impl.ApplicationImpl;
import com.intellij.openapi.components.ExportableApplicationComponent;
import com.intellij.openapi.components.StateStorage;
import com.intellij.openapi.components.StateStorageException;
import com.intellij.openapi.components.TrackingPathMacroSubstitutor;
import com.intellij.openapi.components.impl.stores.IComponentStore;
import com.intellij.openapi.components.impl.stores.IProjectStore;
import com.intellij.openapi.components.impl.stores.StorageUtil;
import com.intellij.openapi.components.impl.stores.XmlElementStorage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.progress.*;
import com.intellij.openapi.progress.util.ProgressWindow;
import com.intellij.openapi.project.*;
import com.intellij.openapi.project.ex.ProjectEx;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.impl.local.FileWatcher;
import com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl;
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeFrame;
import com.intellij.util.Alarm;
import com.intellij.util.ArrayUtil;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.UIUtil;
import gnu.trove.THashSet;
import gnu.trove.TObjectLongHashMap;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ProjectManagerImpl extends ProjectManagerEx implements NamedJDOMExternalizable, ExportableApplicationComponent {
private static final Logger LOG = Logger.getInstance("#com.intellij.project.impl.ProjectManagerImpl");
public static final int CURRENT_FORMAT_VERSION = 4;
private static final Key<List<ProjectManagerListener>> LISTENERS_IN_PROJECT_KEY = Key.create("LISTENERS_IN_PROJECT_KEY");
private static final String ELEMENT_DEFAULT_PROJECT = "defaultProject";
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
private ProjectImpl myDefaultProject; // Only used asynchronously in save and dispose, which itself are synchronized.
@SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"})
private Element myDefaultProjectRootElement; // Only used asynchronously in save and dispose, which itself are synchronized.
private final List<Project> myOpenProjects = new ArrayList<Project>();
private Project[] myOpenProjectsArrayCache = {};
private final List<ProjectManagerListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private final Set<Project> myTestProjects = new THashSet<Project>();
private final Map<VirtualFile, byte[]> mySavedCopies = new HashMap<VirtualFile, byte[]>();
private final TObjectLongHashMap<VirtualFile> mySavedTimestamps = new TObjectLongHashMap<VirtualFile>();
private final Map<Project, List<Pair<VirtualFile, StateStorage>>> myChangedProjectFiles =
new HashMap<Project, List<Pair<VirtualFile, StateStorage>>>();
private final Alarm myChangedFilesAlarm = new Alarm();
private final List<Pair<VirtualFile, StateStorage>> myChangedApplicationFiles = new ArrayList<Pair<VirtualFile, StateStorage>>();
private final AtomicInteger myReloadBlockCount = new AtomicInteger(0);
private final ProgressManager myProgressManager;
private volatile boolean myDefaultProjectWasDisposed = false;
@NotNull
private static List<ProjectManagerListener> getListeners(Project project) {
List<ProjectManagerListener> array = project.getUserData(LISTENERS_IN_PROJECT_KEY);
if (array == null) return Collections.emptyList();
return array;
}
/** @noinspection UnusedParameters*/
public ProjectManagerImpl(VirtualFileManager virtualFileManager,
RecentProjectsManagerBase recentProjectsManager,
ProgressManager progressManager) {
myProgressManager = progressManager;
Application app = ApplicationManager.getApplication();
MessageBus messageBus = app.getMessageBus();
messageBus.connect(app).subscribe(StateStorage.STORAGE_TOPIC, new StateStorage.Listener() {
@Override
public void storageFileChanged(@NotNull final VirtualFileEvent event, @NotNull final StateStorage storage) {
VirtualFile file = event.getFile();
if (!file.isDirectory() && !(event.getRequestor() instanceof StateStorage.SaveSession)) {
saveChangedProjectFile(file, null, storage);
}
}
});
final ProjectManagerListener busPublisher = messageBus.syncPublisher(TOPIC);
addProjectManagerListener(
new ProjectManagerListener() {
@Override
public void projectOpened(final Project project) {
MessageBus messageBus = project.getMessageBus();
MessageBusConnection connection = messageBus.connect(project);
connection.subscribe(StateStorage.STORAGE_TOPIC, new StateStorage.Listener() {
@Override
public void storageFileChanged(@NotNull final VirtualFileEvent event, @NotNull final StateStorage storage) {
VirtualFile file = event.getFile();
if (!file.isDirectory() && !(event.getRequestor() instanceof StateStorage.SaveSession)) {
saveChangedProjectFile(file, project, storage);
}
}
});
busPublisher.projectOpened(project);
for (ProjectManagerListener listener : getListeners(project)) {
listener.projectOpened(project);
}
}
@Override
public void projectClosed(Project project) {
busPublisher.projectClosed(project);
for (ProjectManagerListener listener : getListeners(project)) {
listener.projectClosed(project);
}
}
@Override
public boolean canCloseProject(Project project) {
for (ProjectManagerListener listener : getListeners(project)) {
if (!listener.canCloseProject(project)) {
return false;
}
}
return true;
}
@Override
public void projectClosing(Project project) {
busPublisher.projectClosing(project);
for (ProjectManagerListener listener : getListeners(project)) {
listener.projectClosing(project);
}
}
}
);
registerExternalProjectFileListener(virtualFileManager);
}
@Override
public void initComponent() { }
@Override
public void disposeComponent() {
ApplicationManager.getApplication().assertWriteAccessAllowed();
Disposer.dispose(myChangedFilesAlarm);
if (myDefaultProject != null) {
Disposer.dispose(myDefaultProject);
myDefaultProject = null;
myDefaultProjectWasDisposed = true;
}
}
private static final boolean LOG_PROJECT_LEAKAGE_IN_TESTS = false;
private static final int MAX_LEAKY_PROJECTS = 42;
@SuppressWarnings("FieldCanBeLocal") private final Map<Project, String> myProjects = new WeakHashMap<Project, String>();
@Override
@Nullable
public Project newProject(final String projectName, @NotNull String filePath, boolean useDefaultProjectSettings, boolean isDummy) {
filePath = toCanonicalName(filePath);
//noinspection ConstantConditions
if (LOG_PROJECT_LEAKAGE_IN_TESTS && ApplicationManager.getApplication().isUnitTestMode()) {
for (int i = 0; i < 42; i++) {
if (myProjects.size() < MAX_LEAKY_PROJECTS) break;
System.gc();
TimeoutUtil.sleep(100);
System.gc();
}
if (myProjects.size() >= MAX_LEAKY_PROJECTS) {
List<Project> copy = new ArrayList<Project>(myProjects.keySet());
myProjects.clear();
throw new TooManyProjectLeakedException(copy);
}
}
ProjectImpl project = createProject(projectName, filePath, false, ApplicationManager.getApplication().isUnitTestMode());
try {
initProject(project, useDefaultProjectSettings ? (ProjectImpl)getDefaultProject() : null);
if (LOG_PROJECT_LEAKAGE_IN_TESTS) {
myProjects.put(project, null);
}
return project;
}
catch (Throwable t) {
LOG.info(t);
Messages.showErrorDialog(message(t), ProjectBundle.message("project.load.default.error"));
return null;
}
}
@NonNls
private static String message(Throwable e) {
String message = e.getMessage();
if (message != null) return message;
message = e.getLocalizedMessage();
if (message != null) return message;
message = e.toString();
Throwable cause = e.getCause();
if (cause != null) {
String causeMessage = message(cause);
return message + " (cause: " + causeMessage + ")";
}
return message;
}
private void initProject(@NotNull ProjectImpl project, @Nullable ProjectImpl template) throws IOException {
ProgressIndicator indicator = myProgressManager.getProgressIndicator();
if (indicator != null && !project.isDefault()) {
indicator.setText(ProjectBundle.message("loading.components.for", project.getName()));
indicator.setIndeterminate(true);
}
ApplicationManager.getApplication().getMessageBus().syncPublisher(ProjectLifecycleListener.TOPIC).beforeProjectLoaded(project);
boolean succeed = false;
try {
if (template != null) {
project.getStateStore().loadProjectFromTemplate(template);
}
else {
project.getStateStore().load();
}
project.loadProjectComponents();
project.init();
succeed = true;
}
finally {
if (!succeed) {
scheduleDispose(project);
}
}
}
private ProjectImpl createProject(@Nullable String projectName,
@NotNull String filePath,
boolean isDefault,
boolean isOptimiseTestLoadSpeed) {
return isDefault ? new DefaultProject(this, "", isOptimiseTestLoadSpeed)
: new ProjectImpl(this, new File(filePath).getAbsolutePath(), isOptimiseTestLoadSpeed, projectName);
}
private static void scheduleDispose(final ProjectImpl project) {
if (project.isDefault()) {
return;
}
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
if (!project.isDisposed()) {
Disposer.dispose(project);
}
}
});
}
});
}
@Override
@Nullable
public Project loadProject(@NotNull String filePath) throws IOException, JDOMException, InvalidDataException {
try {
ProjectImpl project = createProject(null, filePath, false, false);
initProject(project, null);
return project;
}
catch (Throwable t) {
LOG.info(t);
throw new IOException(t);
}
}
@NotNull
private static String toCanonicalName(@NotNull final String filePath) {
try {
return FileUtil.resolveShortWindowsName(filePath);
}
catch (IOException e) {
// OK. File does not yet exist so it's canonical path will be equal to its original path.
}
return filePath;
}
@TestOnly
public synchronized boolean isDefaultProjectInitialized() {
return myDefaultProject != null;
}
@Override
@NotNull
public synchronized Project getDefaultProject() {
LOG.assertTrue(!myDefaultProjectWasDisposed, "Default project has been already disposed!");
if (myDefaultProject == null) {
ProgressManager.getInstance().executeNonCancelableSection(new Runnable() {
@Override
public void run() {
try {
myDefaultProject = createProject(null, "", true, ApplicationManager.getApplication().isUnitTestMode());
initProject(myDefaultProject, null);
myDefaultProjectRootElement = null;
}
catch (Throwable t) {
PluginManager.processException(t);
}
}
});
}
return myDefaultProject;
}
@Nullable
public Element getDefaultProjectRootElement() {
return myDefaultProjectRootElement;
}
@Override
@NotNull
public Project[] getOpenProjects() {
synchronized (myOpenProjects) {
if (myOpenProjectsArrayCache.length != myOpenProjects.size()) {
LOG.error("Open projects: " + myOpenProjects + "; cache: " + Arrays.asList(myOpenProjectsArrayCache));
}
if (myOpenProjectsArrayCache.length > 0 && myOpenProjectsArrayCache[0] != myOpenProjects.get(0)) {
LOG.error("Open projects cache corrupted. Open projects: " + myOpenProjects + "; cache: " + Arrays.asList(myOpenProjectsArrayCache));
}
if (ApplicationManager.getApplication().isUnitTestMode()) {
Project[] testProjects = myTestProjects.toArray(new Project[myTestProjects.size()]);
for (Project testProject : testProjects) {
assert !testProject.isDisposed() : testProject;
}
return ArrayUtil.mergeArrays(myOpenProjectsArrayCache, testProjects);
}
return myOpenProjectsArrayCache;
}
}
@Override
public boolean isProjectOpened(Project project) {
synchronized (myOpenProjects) {
return ApplicationManager.getApplication().isUnitTestMode() && myTestProjects.contains(project) || myOpenProjects.contains(project);
}
}
@Override
public boolean openProject(final Project project) {
if (isLight(project)) {
throw new AssertionError("must not open light project");
}
final Application application = ApplicationManager.getApplication();
if (!application.isUnitTestMode() && !((ProjectEx)project).getStateStore().checkVersion()) {
return false;
}
synchronized (myOpenProjects) {
if (myOpenProjects.contains(project)) {
return false;
}
myOpenProjects.add(project);
cacheOpenProjects();
}
fireProjectOpened(project);
DumbService.getInstance(project).queueTask(new DumbModeTask() {
@Override
public void performInDumbMode(@NotNull ProgressIndicator indicator) {
waitForFileWatcher(indicator);
}
@Override
public String toString() {
return "wait for file watcher";
}
});
final StartupManagerImpl startupManager = (StartupManagerImpl)StartupManager.getInstance(project);
boolean ok = myProgressManager.runProcessWithProgressSynchronously(new Runnable() {
@Override
public void run() {
startupManager.runStartupActivities();
// dumb mode should start before post-startup activities
// only when startCacheUpdate is called from UI thread, we can guarantee that
// when the method returns, the application has entered dumb mode
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
startupManager.startCacheUpdate();
}
});
startupManager.runPostStartupActivitiesFromExtensions();
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
startupManager.runPostStartupActivities();
}
});
}
}, ProjectBundle.message("project.load.progress"), canCancelProjectLoading(), project);
if (!ok) {
closeProject(project, false, false, true);
notifyProjectOpenFailed();
return false;
}
if (!application.isHeadlessEnvironment() && !application.isUnitTestMode()) {
// should be invoked last
startupManager.runWhenProjectIsInitialized(new Runnable() {
@Override
public void run() {
final TrackingPathMacroSubstitutor macroSubstitutor =
((ProjectEx)project).getStateStore().getStateStorageManager().getMacroSubstitutor();
if (macroSubstitutor != null) {
StorageUtil.notifyUnknownMacros(macroSubstitutor, project, null);
}
}
});
}
return true;
}
private static boolean canCancelProjectLoading() {
ProgressIndicator indicator = ProgressIndicatorProvider.getGlobalProgressIndicator();
return !(indicator instanceof NonCancelableSection);
}
private void cacheOpenProjects() {
myOpenProjectsArrayCache = myOpenProjects.toArray(new Project[myOpenProjects.size()]);
}
private static void waitForFileWatcher(ProgressIndicator indicator) {
LocalFileSystem fs = LocalFileSystem.getInstance();
if (!(fs instanceof LocalFileSystemImpl)) return;
final FileWatcher watcher = ((LocalFileSystemImpl)fs).getFileWatcher();
if (!watcher.isOperational() || !watcher.isSettingRoots()) return;
LOG.info("FW/roots waiting started");
indicator.setIndeterminate(true);
indicator.setText(ProjectBundle.message("project.load.waiting.watcher"));
if (indicator instanceof ProgressWindow) {
((ProgressWindow)indicator).setCancelButtonText(CommonBundle.message("button.skip"));
}
while (watcher.isSettingRoots() && !indicator.isCanceled()) {
TimeoutUtil.sleep(10);
}
LOG.info("FW/roots waiting finished");
}
@Override
public Project loadAndOpenProject(@NotNull final String filePath) throws IOException {
final Project project = convertAndLoadProject(filePath);
if (project == null) {
WelcomeFrame.showIfNoProjectOpened();
return null;
}
// todo unify this logic with PlatformProjectOpenProcessor
if (!openProject(project)) {
WelcomeFrame.showIfNoProjectOpened();
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
Disposer.dispose(project);
}
});
}
return project;
}
/**
* Converts and loads the project at the specified path.
*
* @param filePath the path to open the project.
* @return the project, or null if the user has cancelled opening the project.
*/
@Override
@Nullable
public Project convertAndLoadProject(String filePath) throws IOException {
final String fp = toCanonicalName(filePath);
final ConversionResult conversionResult = ConversionService.getInstance().convert(fp);
if (conversionResult.openingIsCanceled()) {
return null;
}
final Project project;
try {
project = loadProjectWithProgress(filePath);
if (project == null) return null;
}
catch (IOException e) {
LOG.info(e);
throw e;
}
catch (Throwable t) {
LOG.info(t);
throw new IOException(t);
}
if (!conversionResult.conversionNotNeeded()) {
StartupManager.getInstance(project).registerPostStartupActivity(new Runnable() {
@Override
public void run() {
conversionResult.postStartupActivity(project);
}
});
}
return project;
}
/**
* Opens the project at the specified path.
*
* @param filePath the path to open the project.
* @return the project, or null if the user has cancelled opening the project.
*/
@Nullable
private Project loadProjectWithProgress(@NotNull final String filePath) throws IOException {
final ProjectImpl project = createProject(null, toCanonicalName(filePath), false, false);
try {
myProgressManager.runProcessWithProgressSynchronously(new ThrowableComputable<Project, IOException>() {
@Override
@Nullable
public Project compute() throws IOException {
initProject(project, null);
return project;
}
}, ProjectBundle.message("project.load.progress"), canCancelProjectLoading(), project);
}
catch (StateStorageException e) {
throw new IOException(e);
}
catch (ProcessCanceledException ignore) {
return null;
}
return project;
}
private static void notifyProjectOpenFailed() {
ApplicationManager.getApplication().getMessageBus().syncPublisher(AppLifecycleListener.TOPIC).projectOpenFailed();
WelcomeFrame.showIfNoProjectOpened();
}
private void registerExternalProjectFileListener(VirtualFileManager virtualFileManager) {
virtualFileManager.addVirtualFileManagerListener(new VirtualFileManagerListener() {
@Override
public void beforeRefreshStart(boolean asynchronous) {
}
@Override
public void afterRefreshFinish(boolean asynchronous) {
scheduleReloadApplicationAndProject();
}
});
}
private void askToReloadProjectIfConfigFilesChangedExternally() {
LOG.debug("[RELOAD] myReloadBlockCount = " + myReloadBlockCount.get());
if (myReloadBlockCount.get() == 0) {
Set<Project> projects;
synchronized (myChangedProjectFiles) {
if (myChangedProjectFiles.isEmpty()) return;
projects = new HashSet<Project>(myChangedProjectFiles.keySet());
}
List<Project> projectsToReload = new ArrayList<Project>();
for (Project project : projects) {
if (shouldReloadProject(project)) {
projectsToReload.add(project);
}
}
for (final Project projectToReload : projectsToReload) {
reloadProjectImpl(projectToReload, false);
}
}
}
private boolean tryToReloadApplication() {
try {
final Application app = ApplicationManager.getApplication();
if (app.isDisposed()) return false;
final HashSet<Pair<VirtualFile, StateStorage>> causes = new HashSet<Pair<VirtualFile, StateStorage>>(myChangedApplicationFiles);
if (causes.isEmpty()) return true;
final boolean[] reloadOk = {false};
final LinkedHashSet<String> components = new LinkedHashSet<String>();
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
try {
reloadOk[0] = ((ApplicationImpl)app).getStateStore().reload(causes, components);
}
catch (StateStorageException e) {
Messages.showWarningDialog(ProjectBundle.message("project.reload.failed", e.getMessage()),
ProjectBundle.message("project.reload.failed.title"));
}
catch (IOException e) {
Messages.showWarningDialog(ProjectBundle.message("project.reload.failed", e.getMessage()),
ProjectBundle.message("project.reload.failed.title"));
}
}
});
if (!reloadOk[0] && !components.isEmpty()) {
String message = "Application components were changed externally and cannot be reloaded:\n";
for (String component : components) {
message += component + "\n";
}
final boolean canRestart = ApplicationManager.getApplication().isRestartCapable();
message += "Would you like to " + (canRestart ? "restart " : "shutdown ");
message += ApplicationNamesInfo.getInstance().getProductName() + "?";
if (Messages.showYesNoDialog(message,
"Application Configuration Reload", Messages.getQuestionIcon()) == Messages.YES) {
for (Pair<VirtualFile, StateStorage> cause : causes) {
StateStorage stateStorage = cause.getSecond();
if (stateStorage instanceof XmlElementStorage) {
((XmlElementStorage)stateStorage).disableSaving();
}
}
ApplicationManagerEx.getApplicationEx().restart(true);
}
}
return reloadOk[0];
}
finally {
myChangedApplicationFiles.clear();
}
}
private boolean shouldReloadProject(final Project project) {
if (project.isDisposed()) return false;
final HashSet<Pair<VirtualFile, StateStorage>> causes = new HashSet<Pair<VirtualFile, StateStorage>>();
synchronized (myChangedProjectFiles) {
final List<Pair<VirtualFile, StateStorage>> changes = myChangedProjectFiles.remove(project);
if (changes != null) {
causes.addAll(changes);
}
if (causes.isEmpty()) return false;
}
final boolean[] reloadOk = {false};
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
try {
LOG.debug("[RELOAD] Reloading project/components...");
reloadOk[0] = ((ProjectEx)project).getStateStore().reload(causes);
}
catch (StateStorageException e) {
Messages.showWarningDialog(ProjectBundle.message("project.reload.failed", e.getMessage()),
ProjectBundle.message("project.reload.failed.title"));
}
catch (IOException e) {
Messages.showWarningDialog(ProjectBundle.message("project.reload.failed", e.getMessage()),
ProjectBundle.message("project.reload.failed.title"));
}
}
});
if (reloadOk[0]) return false;
String message;
if (causes.size() == 1) {
message = ProjectBundle.message("project.reload.external.change.single", causes.iterator().next().first.getPresentableUrl());
}
else {
StringBuilder filesBuilder = new StringBuilder();
boolean first = true;
Set<String> alreadyShown = new HashSet<String>();
for (Pair<VirtualFile, StateStorage> cause : causes) {
String url = cause.first.getPresentableUrl();
if (!alreadyShown.contains(url)) {
if (alreadyShown.size() > 10) {
filesBuilder.append("\n" + "and ").append(causes.size() - alreadyShown.size()).append(" more");
break;
}
if (!first) filesBuilder.append("\n");
first = false;
filesBuilder.append(url);
alreadyShown.add(url);
}
}
message = ProjectBundle.message("project.reload.external.change.multiple", filesBuilder.toString());
}
return Messages.showDialog(message,
ProjectBundle.message("project.reload.external.change.title"),
new String[]{"&Reload Project", "&Discard Changes"},
-1,
Messages.getQuestionIcon()) == 0;
}
@Override
public boolean isFileSavedToBeReloaded(VirtualFile candidate) {
return mySavedCopies.containsKey(candidate);
}
@Override
public void blockReloadingProjectOnExternalChanges() {
myReloadBlockCount.incrementAndGet();
}
@Override
public void unblockReloadingProjectOnExternalChanges() {
if (myReloadBlockCount.decrementAndGet() == 0) scheduleReloadApplicationAndProject();
}
private void scheduleReloadApplicationAndProject() {
// todo: commented due to "IDEA-61938 Libraries configuration is kept if switching branches"
// because of save which may happen _before_ project reload ;(
//ApplicationManager.getApplication().invokeLater(new Runnable() {
// public void run() {
//IdeEventQueue.getInstance().addIdleListener(new Runnable() {
// @Override
// public void run() {
// IdeEventQueue.getInstance().removeIdleListener(this);
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
if (!tryToReloadApplication()) return;
askToReloadProjectIfConfigFilesChangedExternally();
}
}, ModalityState.NON_MODAL);
//}
//}, 2000);
//}
//}, ModalityState.NON_MODAL);
}
@Override
public void openTestProject(@NotNull final Project project) {
synchronized (myOpenProjects) {
assert ApplicationManager.getApplication().isUnitTestMode();
assert !project.isDisposed() : "Must not open already disposed project";
myTestProjects.add(project);
}
}
@Override
public Collection<Project> closeTestProject(@NotNull Project project) {
synchronized (myOpenProjects) {
assert ApplicationManager.getApplication().isUnitTestMode();
myTestProjects.remove(project);
return myTestProjects;
}
}
@Override
public void saveChangedProjectFile(final VirtualFile file, final Project project) {
if (file.exists()) {
copyToTemp(file);
}
registerProjectToReload(project, file, null);
}
private void saveChangedProjectFile(final VirtualFile file, @Nullable final Project project, final StateStorage storage) {
if (file.exists()) {
copyToTemp(file);
}
registerProjectToReload(project, file, storage);
}
private void registerProjectToReload(@Nullable final Project project, final VirtualFile cause, @Nullable final StateStorage storage) {
if (LOG.isDebugEnabled()) {
LOG.debug("[RELOAD] Registering project to reload: " + cause, new Exception());
}
if (project != null) {
synchronized (myChangedProjectFiles) {
List<Pair<VirtualFile, StateStorage>> changedProjectFiles = myChangedProjectFiles.get(project);
if (changedProjectFiles == null) {
changedProjectFiles = new ArrayList<Pair<VirtualFile, StateStorage>>();
myChangedProjectFiles.put(project, changedProjectFiles);
}
changedProjectFiles.add(Pair.create(cause, storage));
}
}
else {
myChangedApplicationFiles.add(Pair.create(cause, storage));
}
myChangedFilesAlarm.cancelAllRequests();
myChangedFilesAlarm.addRequest(new Runnable() {
@Override
public void run() {
LOG.debug("[RELOAD] Scheduling reload application & project, myReloadBlockCount = " + myReloadBlockCount);
if (myReloadBlockCount.get() == 0) {
scheduleReloadApplicationAndProject();
}
}
}, 444);
}
private void copyToTemp(VirtualFile file) {
try {
final byte[] bytes = file.contentsToByteArray();
mySavedCopies.put(file, bytes);
mySavedTimestamps.put(file, file.getTimeStamp());
}
catch (IOException e) {
LOG.error(e);
}
}
private void restoreCopy(VirtualFile file) {
try {
if (file == null) return; // Externally deleted actually.
if (!file.isWritable()) return; // IDEA was unable to save it as well. So no need to restore.
final byte[] bytes = mySavedCopies.get(file);
if (bytes != null) {
try {
file.setBinaryContent(bytes, -1, mySavedTimestamps.get(file));
}
catch (IOException e) {
Messages.showWarningDialog(ProjectBundle.message("project.reload.write.failed", file.getPresentableUrl()),
ProjectBundle.message("project.reload.write.failed.title"));
}
}
}
finally {
mySavedCopies.remove(file);
mySavedTimestamps.remove(file);
}
}
@Override
public void reloadProject(@NotNull final Project p) {
reloadProjectImpl(p, true);
}
public void reloadProjectImpl(@NotNull final Project p, final boolean clearCopyToRestore) {
if (clearCopyToRestore) {
mySavedCopies.clear();
mySavedTimestamps.clear();
}
final Project[] project = {p};
ProjectReloadState.getInstance(project[0]).onBeforeAutomaticProjectReload();
final Application application = ApplicationManager.getApplication();
application.invokeLater(new Runnable() {
@Override
public void run() {
LOG.debug("Reloading project.");
ProjectImpl projectImpl = (ProjectImpl)project[0];
if (projectImpl.isDisposed()) return;
IProjectStore projectStore = projectImpl.getStateStore();
final String location = projectImpl.getPresentableUrl();
final List<File> original;
try {
IComponentStore.SaveSession saveSession = projectStore.startSave();
original = saveSession.getAllStorageFiles(true);
saveSession.finishSave();
}
catch (IOException e) {
LOG.error(e);
return;
}
if (project[0].isDisposed() || ProjectUtil.closeAndDispose(project[0])) {
application.runWriteAction(new Runnable() {
@Override
public void run() {
for (File originalFile : original) {
restoreCopy(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(originalFile));
}
}
});
project[0] = null; // Let it go.
ProjectUtil.openProject(location, null, true);
}
}
}, ModalityState.NON_MODAL);
}
@Override
public boolean closeProject(@NotNull final Project project) {
return closeProject(project, true, false, true);
}
public boolean closeProject(@NotNull final Project project, final boolean save, final boolean dispose, boolean checkCanClose) {
if (isLight(project)) {
throw new AssertionError("must not close light project");
}
if (!isProjectOpened(project)) return true;
if (checkCanClose && !canClose(project)) return false;
final ShutDownTracker shutDownTracker = ShutDownTracker.getInstance();
shutDownTracker.registerStopperThread(Thread.currentThread());
try {
if (save) {
FileDocumentManager.getInstance().saveAllDocuments();
project.save();
}
if (checkCanClose && !ensureCouldCloseIfUnableToSave(project)) {
return false;
}
fireProjectClosing(project); // somebody can start progress here, do not wrap in write action
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
synchronized (myOpenProjects) {
myOpenProjects.remove(project);
cacheOpenProjects();
myTestProjects.remove(project);
}
myChangedProjectFiles.remove(project);
fireProjectClosed(project);
if (dispose) {
Disposer.dispose(project);
}
}
});
}
finally {
shutDownTracker.unregisterStopperThread(Thread.currentThread());
}
return true;
}
public static boolean isLight(@NotNull Project project) {
return ApplicationManager.getApplication().isUnitTestMode() && project.toString().contains("light_temp_");
}
@Override
public boolean closeAndDispose(@NotNull final Project project) {
return closeProject(project, true, true, true);
}
private void fireProjectClosing(Project project) {
if (LOG.isDebugEnabled()) {
LOG.debug("enter: fireProjectClosing()");
}
for (ProjectManagerListener listener : myListeners) {
try {
listener.projectClosing(project);
}
catch (Exception e) {
LOG.error(e);
}
}
}
@Override
public void addProjectManagerListener(@NotNull ProjectManagerListener listener) {
myListeners.add(listener);
}
@Override
public void addProjectManagerListener(@NotNull final ProjectManagerListener listener, @NotNull Disposable parentDisposable) {
addProjectManagerListener(listener);
Disposer.register(parentDisposable, new Disposable() {
@Override
public void dispose() {
removeProjectManagerListener(listener);
}
});
}
@Override
public void removeProjectManagerListener(@NotNull ProjectManagerListener listener) {
boolean removed = myListeners.remove(listener);
LOG.assertTrue(removed);
}
@Override
public void addProjectManagerListener(@NotNull Project project, @NotNull ProjectManagerListener listener) {
List<ProjectManagerListener> listeners = project.getUserData(LISTENERS_IN_PROJECT_KEY);
if (listeners == null) {
listeners = ((UserDataHolderEx)project)
.putUserDataIfAbsent(LISTENERS_IN_PROJECT_KEY, ContainerUtil.<ProjectManagerListener>createLockFreeCopyOnWriteList());
}
listeners.add(listener);
}
@Override
public void removeProjectManagerListener(@NotNull Project project, @NotNull ProjectManagerListener listener) {
List<ProjectManagerListener> listeners = project.getUserData(LISTENERS_IN_PROJECT_KEY);
LOG.assertTrue(listeners != null);
boolean removed = listeners.remove(listener);
LOG.assertTrue(removed);
}
private void fireProjectOpened(Project project) {
if (LOG.isDebugEnabled()) {
LOG.debug("projectOpened");
}
for (ProjectManagerListener listener : myListeners) {
try {
listener.projectOpened(project);
}
catch (Exception e) {
LOG.error(e);
}
}
}
private void fireProjectClosed(Project project) {
if (LOG.isDebugEnabled()) {
LOG.debug("projectClosed");
}
for (ProjectManagerListener listener : myListeners) {
try {
listener.projectClosed(project);
}
catch (Exception e) {
LOG.error(e);
}
}
}
@Override
public boolean canClose(Project project) {
if (LOG.isDebugEnabled()) {
LOG.debug("enter: canClose()");
}
for (ProjectManagerListener listener : myListeners) {
try {
if (!listener.canCloseProject(project)) return false;
}
catch (Throwable e) {
LOG.warn(e); // DO NOT LET ANY PLUGIN to prevent closing due to exception
}
}
return true;
}
private static boolean ensureCouldCloseIfUnableToSave(@NotNull final Project project) {
final ProjectImpl.UnableToSaveProjectNotification[] notifications =
NotificationsManager.getNotificationsManager().getNotificationsOfType(ProjectImpl.UnableToSaveProjectNotification.class, project);
if (notifications.length == 0) return true;
final String fileNames = StringUtil.join(notifications[0].getFileNames(), "\n");
final String msg = String.format("%s was unable to save some project files,\nare you sure you want to close this project anyway?",
ApplicationNamesInfo.getInstance().getProductName());
return Messages.showDialog(project, msg, "Unsaved Project", "Read-only files:\n\n" + fileNames, new String[]{"Yes", "No"}, 0, 1,
Messages.getWarningIcon()) == 0;
}
@Override
public void writeExternal(Element parentNode) {
if (myDefaultProject != null) {
myDefaultProject.save();
}
if (myDefaultProjectRootElement != null) {
myDefaultProjectRootElement.detach();
parentNode.addContent(myDefaultProjectRootElement);
}
}
public void setDefaultProjectRootElement(final Element defaultProjectRootElement) {
myDefaultProjectRootElement = defaultProjectRootElement;
}
@Override
public void readExternal(Element parentNode) {
myDefaultProjectRootElement = parentNode.getChild(ELEMENT_DEFAULT_PROJECT);
if (myDefaultProjectRootElement != null) {
myDefaultProjectRootElement.detach();
}
}
@Override
public String getExternalFileName() {
return "project.default";
}
@Override
@NotNull
public String getComponentName() {
return "ProjectManager";
}
@Override
@NotNull
public File[] getExportFiles() {
return new File[]{PathManager.getOptionsFile(this)};
}
@Override
@NotNull
public String getPresentableName() {
return ProjectBundle.message("project.default.settings");
}
}