blob: dd6fe984409b9e5d78bfdc399debec2b50111926 [file] [log] [blame]
/*
* Copyright 2000-2013 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.externalSystem.service.project.autoimport;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.externalSystem.ExternalSystemAutoImportAware;
import com.intellij.openapi.externalSystem.ExternalSystemManager;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.ProjectSystemId;
import com.intellij.openapi.externalSystem.model.project.ProjectData;
import com.intellij.openapi.externalSystem.service.project.ExternalProjectRefreshCallback;
import com.intellij.openapi.externalSystem.service.project.manage.ProjectDataManager;
import com.intellij.openapi.externalSystem.settings.AbstractExternalSystemSettings;
import com.intellij.openapi.externalSystem.settings.ExternalProjectSettings;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.externalSystem.util.ExternalSystemConstants;
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil;
import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
import com.intellij.util.Alarm;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.ContainerUtilRt;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author Denis Zhdanov
* @since 6/7/13 6:38 PM
*/
public class ExternalSystemAutoImporter implements BulkFileListener, DocumentListener {
@NotNull private final ConcurrentMap<ProjectSystemId, Set<String /* external project path */>> myFilesToRefresh
= ContainerUtil.newConcurrentMap();
@NotNull private final Alarm myVfsAlarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD);
@NotNull private final ReadWriteLock myVfsLock = new ReentrantReadWriteLock();
@NotNull private final Set<Document> myDocumentsToSave = ContainerUtilRt.newHashSet();
@NotNull private final Alarm myDocumentAlarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD);
@NotNull private final ReadWriteLock myDocumentLock = new ReentrantReadWriteLock();
@NotNull private final Runnable myFilesRequest = new Runnable() {
@Override
public void run() {
refreshFilesIfNecessary();
}
};
@NotNull private final Runnable myDocumentsSaveRequest = new Runnable() {
@Override
public void run() {
saveDocumentsIfNecessary();
}
};
@NotNull private final ExternalProjectRefreshCallback myRefreshCallback = new ExternalProjectRefreshCallback() {
@Override
public void onSuccess(@Nullable final DataNode<ProjectData> externalProject) {
if (externalProject != null) {
ExternalSystemApiUtil.executeProjectChangeAction(new Runnable() {
@Override
public void run() {
ProjectRootManagerEx.getInstanceEx(myProject).mergeRootsChangesDuring(new Runnable() {
@Override
public void run() {
myProjectDataManager.importData(externalProject.getKey(), Collections.singleton(externalProject), myProject, true);
}
});
}
});
}
}
@Override
public void onFailure(@NotNull String errorMessage, @Nullable String errorDetails) {
// Do nothing.
}
};
@NotNull private final Project myProject;
@NotNull private final ProjectDataManager myProjectDataManager;
@NotNull private final MyEntry[] myAutoImportAware;
public ExternalSystemAutoImporter(@NotNull Project project,
@NotNull ProjectDataManager projectDataManager,
@NotNull MyEntry[] autoImportAware)
{
myProject = project;
myProjectDataManager = projectDataManager;
myAutoImportAware = autoImportAware;
}
@SuppressWarnings("unchecked")
public static void letTheMagicBegin(@NotNull Project project) {
List<MyEntry> autoImportAware = ContainerUtilRt.newArrayList();
Collection<ExternalSystemManager<?, ?, ?, ?, ?>> managers = ExternalSystemApiUtil.getAllManagers();
for (ExternalSystemManager<?, ?, ?, ?, ?> manager : managers) {
AbstractExternalSystemSettings<?, ?, ?> systemSettings = manager.getSettingsProvider().fun(project);
ExternalSystemAutoImportAware defaultImportAware = createDefault(systemSettings);
final ExternalSystemAutoImportAware aware;
if (manager instanceof ExternalSystemAutoImportAware) {
aware = combine(defaultImportAware, (ExternalSystemAutoImportAware)manager);
}
else {
aware = defaultImportAware;
}
autoImportAware.add(new MyEntry(manager.getSystemId(), systemSettings, aware));
}
MyEntry[] entries = autoImportAware.toArray(new MyEntry[autoImportAware.size()]);
ExternalSystemAutoImporter autoImporter = new ExternalSystemAutoImporter(
project,
ServiceManager.getService(ProjectDataManager.class),
entries
);
final MessageBus messageBus = project.getMessageBus();
messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, autoImporter);
EditorFactory.getInstance().getEventMulticaster().addDocumentListener(autoImporter, project);
}
@NotNull
private static ExternalSystemAutoImportAware combine(@NotNull final ExternalSystemAutoImportAware aware1,
@NotNull final ExternalSystemAutoImportAware aware2)
{
return new ExternalSystemAutoImportAware() {
@Nullable
@Override
public String getAffectedExternalProjectPath(@NotNull String changedFileOrDirPath, @NotNull Project project) {
String projectPath = aware1.getAffectedExternalProjectPath(changedFileOrDirPath, project);
return projectPath == null ? aware2.getAffectedExternalProjectPath(changedFileOrDirPath, project) : projectPath;
}
};
}
@NotNull
private static ExternalSystemAutoImportAware createDefault(@NotNull final AbstractExternalSystemSettings<?, ?, ?> systemSettings) {
return new ExternalSystemAutoImportAware() {
@Nullable
@Override
public String getAffectedExternalProjectPath(@NotNull String changedFileOrDirPath, @NotNull Project project) {
return systemSettings.getLinkedProjectSettings(changedFileOrDirPath) == null ? null : changedFileOrDirPath;
}
};
}
@Override
public void beforeDocumentChange(DocumentEvent event) {
}
@Override
public void documentChanged(DocumentEvent event) {
Document document = event.getDocument();
FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
VirtualFile file = fileDocumentManager.getFile(document);
if (file == null) {
return;
}
String path = ExternalSystemApiUtil.getLocalFileSystemPath(file);
for (MyEntry entry : myAutoImportAware) {
if (entry.aware.getAffectedExternalProjectPath(path, myProject) != null) {
// Document save triggers VFS event but FileDocumentManager might be registered after the current listener, that's why
// call to 'saveDocument()' might not produce the desired effect. That's why we reschedule document save if necessary.
scheduleDocumentSave(document);
return;
}
}
}
private void scheduleDocumentSave(@NotNull Document document) {
Lock lock = myDocumentLock.readLock();
lock.lock();
try {
myDocumentsToSave.add(document);
if (myDocumentAlarm.getActiveRequestCount() <= 0) {
myDocumentAlarm.addRequest(myDocumentsSaveRequest, 100);
}
}
finally {
lock.unlock();
}
}
private void saveDocumentsIfNecessary() {
final FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
Lock lock = myDocumentLock.writeLock();
Set<Document> toKeep = ContainerUtilRt.newHashSet();
final Set<Document> toSave = ContainerUtilRt.newHashSet();
lock.lock();
try {
myDocumentAlarm.cancelAllRequests();
for (Document document : myDocumentsToSave) {
if (fileDocumentManager.isDocumentUnsaved(document)) {
toSave.add(document);
}
else {
toKeep.add(document);
}
}
myDocumentsToSave.clear();
if (!toSave.isEmpty()) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
for (Document document : toSave) {
fileDocumentManager.saveDocument(document);
}
}
});
}
if (!toKeep.isEmpty()) {
myDocumentsToSave.addAll(toKeep);
myDocumentAlarm.addRequest(myDocumentsSaveRequest, 100);
}
}
finally {
lock.unlock();
}
}
@Override
public void before(@NotNull List<? extends VFileEvent> events) {
}
@Override
public void after(@NotNull List<? extends VFileEvent> events) {
boolean scheduleRefresh = false;
for (VFileEvent event : events) {
String changedPath = event.getPath();
for (MyEntry entry : myAutoImportAware) {
String projectPath = entry.aware.getAffectedExternalProjectPath(changedPath, myProject);
if (projectPath == null) {
continue;
}
ExternalProjectSettings projectSettings = entry.systemSettings.getLinkedProjectSettings(projectPath);
if (projectSettings != null && projectSettings.isUseAutoImport()) {
addPath(entry.externalSystemId, projectPath);
scheduleRefresh = true;
break;
}
}
}
if (scheduleRefresh) {
myVfsAlarm.cancelAllRequests();
myVfsAlarm.addRequest(myFilesRequest, ExternalSystemConstants.AUTO_IMPORT_DELAY_MILLIS);
}
}
private void addPath(@NotNull ProjectSystemId externalSystemId, @NotNull String path) {
Lock lock = myVfsLock.readLock();
lock.lock();
try {
Set<String> paths = myFilesToRefresh.get(externalSystemId);
while (paths == null) {
myFilesToRefresh.putIfAbsent(externalSystemId, ContainerUtilRt.<String>newHashSet());
paths = myFilesToRefresh.get(externalSystemId);
}
paths.add(path);
}
finally {
lock.unlock();
}
}
private void refreshFilesIfNecessary() {
if (myFilesToRefresh.isEmpty()) {
return;
}
Map<ProjectSystemId, Set<String>> copy = ContainerUtilRt.newHashMap();
Lock fileLock = myVfsLock.writeLock();
fileLock.lock();
try {
copy.putAll(myFilesToRefresh);
myFilesToRefresh.clear();
}
finally {
fileLock.unlock();
}
FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
LocalFileSystem fileSystem = LocalFileSystem.getInstance();
Lock documentLock = myDocumentLock.writeLock();
documentLock.lock();
try {
for (Set<String> paths : copy.values()) {
for (String path : paths) {
VirtualFile file = fileSystem.findFileByPath(path);
if (file != null) {
Document document = fileDocumentManager.getCachedDocument(file);
if (document != null) {
myDocumentsToSave.remove(document);
}
}
}
}
}
finally {
documentLock.unlock();
}
for (Map.Entry<ProjectSystemId, Set<String>> entry : copy.entrySet()) {
for (String path : entry.getValue()) {
ExternalSystemUtil.refreshProject(myProject, entry.getKey(), path, myRefreshCallback, false, ProgressExecutionMode.IN_BACKGROUND_ASYNC, false);
}
}
}
private static class MyEntry {
@NotNull public final ProjectSystemId externalSystemId;
@NotNull public final AbstractExternalSystemSettings<?, ?, ?> systemSettings;
@NotNull public final ExternalSystemAutoImportAware aware;
MyEntry(@NotNull ProjectSystemId externalSystemId,
@NotNull AbstractExternalSystemSettings<?, ?, ?> systemSettings,
@NotNull ExternalSystemAutoImportAware aware)
{
this.externalSystemId = externalSystemId;
this.systemSettings = systemSettings;
this.aware = aware;
}
}
}